原文zh.annas-archive.org/md5/EBC7126C5733D51726286A656704EE51译者飞龙协议CC BY-NC-SA 4.0第八章Reason 中的单元测试在像 Reason 这样的类型化语言中进行测试是一个颇具争议的话题。有些人认为一个良好的测试套件会减少对类型系统的需求。另一方面有些人更看重类型系统而不是他们的测试套件。这些意见上的差异可能导致一些激烈的辩论。当然类型和测试并不是互斥的。我们可以同时拥有类型和测试。也许 Reason 核心团队成员之一郑楼说得最好。测试。这很容易对吧类型会减少一类测试的数量但并不是所有测试。这是一个人们不够重视的讨论。他们总是把测试和类型对立起来。关键是如果你有类型并且添加了测试你的测试将能够用更少的精力表达更多。你不再需要断言无效的输入。你可以断言更重要的东西。如果你想要测试可以存在你只是用它们表达了更多。郑楼您可以在以下 URL 上观看郑楼在 2017 年 React Conf 的演讲youtu.be/_0T5OSSzxms在本章中我们将通过bs-jestBuckleScript 绑定来设置流行的 JavaScript 测试框架 Jest。我们将进行以下操作学习如何使用es6和commonjs模块格式设置bs-jest对 Reason 函数进行单元测试看看编写测试如何帮助我们改进我们的代码要跟着做克隆这本书的 GitHub 存储库并从Chapter08/app-start开始使用以下代码git clone https://github.com/PacktPublishing/ReasonML-Quick-Start-Guide.git cd ReasonML-Quick-Start-Guide cd Chapter08/app-start npm install使用 Jest 进行测试Jest也是由 Facebook 创建的可以说是最受欢迎的 JavaScript 测试框架之一。如果你熟悉 React你可能也熟悉 Jest。因此我们将跳过正式介绍开始在 Reason 中使用 Jest。安装就像任何其他包一样我们从Reason Package Index或简称为Redex开始。Reason 包索引redex.github.io/输入jest会显示到 Jest 的bs-jest绑定。按照bs-jest的安装说明我们首先用 npm 安装bs-jestnpm install--save-dev glennsl/bs-jest然后我们通过在bsconfig.json中包含它来让 BuckleScript 知道这个开发依赖项。请注意键是bs-dev-dependencies而不是bs-dependenciesbs-dev-dependencies:[glennsl/bs-jest]由于bs-jest将jest列为依赖项npm 也会安装jest因此我们不需要将jest作为应用程序的直接依赖项。现在让我们创建一个__tests__目录作为src目录的同级目录cd Chapter08/app-start mkdir __tests__并告诉 BuckleScript 查找这个目录/* bsconfig.json */...sources:[{dir:src,subdirs:true},{dir:__tests__,type:dev}],...最后我们将更新package.json中的test脚本以使用 Jest/* package.json */test:jest我们的第一个测试让我们在__tests__/First_test.re中创建我们的第一个测试暂时使用一些简单的内容/* __tests__/First_test.re */open Jest;describe(Expect,()Expect.(test(toBe,()expect(12)|toBe(3))));现在运行npm test会出现以下错误FAILlib/es6/__tests__/First_test.bs.js ● Test suite failed to run Jest encountered an unexpected token This usually means that you are trying toimporta file which Jest cannot parse,e.g.its not plain JavaScript.Bydefault,ifJest sees a Babel config,it will use that to transform your files,ignoringnode_modules.Heres what you cando:• To have someofyournode_modulesfiles transformed,you can specify a customtransformIgnorePatternsinyour config.• If you need a custom transformation specify atransformoptioninyour config.• If you simply want to mock your non-JSmodules(e.g.binary assets)you can stub them outwiththemoduleNameMapperconfig option.Youll find more details and examplesofthese config optionsinthedocs:https://jestjs.io/docs/en/configuration.htmlDetails:.../lib/es6/__tests__/First_test.bs.js:3import*asJestfromglennsl/bs-jest/lib/es6/src/jest.js;^SyntaxError:Unexpected token*at ScriptTransformer._transformAndBuildScript(node_modules/jest-runtime/build/script_transformer.js:403:17)Test Suites:1failed,1totalTests:0totalSnapshots:0totalTime:1.43s Ran all test suites.npmERR!Test failed.See aboveformore details.问题在于 Jest 无法直接理解 ES 模块格式。记住我们已经通过以下配置参见第二章设置开发环境配置了 BuckleScript 使用 ES 模块/* bsconfig.json */...package-specs:[{module:es6}],...解决这个问题的一种方法是将 BuckleScript 配置为使用commonjs模块格式/* bsconfig.json */...package-specs:[{module:commonjs}],...然后我们还需要更新 webpack 的entry字段/* webpack.config.js */...entry:./lib/js/src/Index.bs.js,/* changed es6 to js */...现在运行npm test会得到一个通过的测试PASSlib/js/__tests__/First_test.bs.js Expect ✓toBe(4ms)Test Suites:1passed,1totalTests:1passed,1totalSnapshots:0totalTime:1.322s Ran all test suites.或者如果我们想继续使用 ES 模块格式我们需要确保 Jest 首先通过 Babel 运行*test.bs.js文件。为此我们需要按照以下步骤进行安装babel-jest和babel-preset-envnpm install babel-core6.26.3babel-jest23.6.0babel-preset-env1.7.0在.babelrc中添加相应的 Babel 配置/* .babelrc */{presets:[env]}确保 Jest 通过 Babel 运行node_modules中的某些第三方依赖。出于性能原因Jest 默认不会通过 Babel 运行node_modules中的任何内容。我们可以通过在package.json中提供自定义的 Jest 配置来覆盖这种行为。在这里我们将告诉 Jest 只忽略不匹配/node_modules/glennsl*、/node_modules/bs-platform*等的第三方依赖/* package.json */...jest:{transformIgnorePatterns:[/node_modules/(?!glennsl|bs-platform|bs-css|reason-react)]}现在使用 ES 模块格式运行npm test可以正常工作PASSlib/es6/__tests__/First_test.bs.js Expect ✓toBe(7ms)Test Suites:1passed,1totalTests:1passed,1totalSnapshots:0totalTime:1.041s Ran all test suites.测试业务逻辑让我们编写一个测试验证我们能够通过id获取正确的顾客。在Customer.re中有一个名为getCustomer的函数接受一个customers数组并通过调用getId来获取id。getId函数接受一个在getCustomer范围之外存在的pathnameletgetCustomercustomers{letidgetId(pathname);customers|Js.Array.find(customercustomer.CustomerType.idid);};我们立即注意到这不是理想的情况。如果getCustomer接受一个customers数组和一个id并专注于通过他们的id获取顾客那将会更好。否则仅仅为getCustomer编写测试会更加困难。因此我们重构getCustomer也接受一个idletgetCustomerById(customers,id){customers|Js.Array.find(customercustomer.CustomerType.idid);};现在我们可以更容易地编写测试。遵循编译器错误确保你已经用getCustomerById替换了getCustomer。对于id参数传入getId(pathname)。让我们将我们的测试重命名为__tests__/Customers_test.re并包括以下测试open Jest;describe(Customer,()Expect.(test(can create a customer,(){letcustomers:array(CustomerType.t)[|{id:1,name:Irita Camsey,address:{street:69 Ryan Parkway,city:Kansas City,state:MO,zip:00494,},phone:8169271752,email:icamsey0over-blog.com,},{id:2,name:Luise Grayson,address:{street:2756 Gale Trail,city:Jacksonville,state:FL,zip:23566,},phone:9044985243,email:lgrayson1netlog.com,},{id:3,name:Derick Whitelaw,address:{street:45 Southridge Par,city:Lexington,state:KY,zip:08037,},phone:4079634850,email:dwhitelaw2fema.gov,},|];letcustomer:CustomerType.tCustomer.getCustomerById(customers,2)|Belt.Option.getExn;expect((customer.id,customer.name))|toEqual((2,Luise Grayson));})));使用现有代码运行这个测试通过npm test会导致以下错误FAILlib/es6/__tests__/Customers_test.bs.js ● Test suite failed to runError:No message was provided Test Suites:1failed,1totalTests:0totalSnapshots:0totalTime:1.711s Ran all test suites.错误的原因是Customers.re在顶层调用了localStorage。/* Customer.re */letcustomersDataBsJson.(parse(getItem(customers)));/* this is the problem */由于 Jest 在 Node.js 中运行我们无法访问浏览器 API。为了解决这个问题我们可以将这个调用包装在一个函数中/* Customer.re */letgetCustomers()DataBsJson.(parse(getItem(customers)));我们可以在initialState中调用这个getCustomers函数。这将使我们能够在 Jest 中避免对localStorage的调用。让我们更新Customer.re将顾客数组移到状态中/* Customer.re */...type state{mode,customer:CustomerType.t,customers:array(CustomerType.t),};...letgetCustomers()DataBsJson.(parse(getItem(customers)));letgetCustomerById(customers,id){customers|Js.Array.find(customercustomer.CustomerType.idid);};...initialState:(){letmodeJs.String.includes(create,pathname)?Create:Update;letcustomersgetCustomers();{mode,customer:switch(mode){|CreategetDefault(customers)|UpdateBelt.Option.getWithDefault(getCustomerById(customers,getId(pathname)),getDefault(customers),)},customers,};},.../* within the reducer */ReasonReact.UpdateWithSideEffects({...state,customer:{id:state.customer.id,name:getInputValue(input[namename]),address:{street:getInputValue(input[namestreet]),city:getInputValue(input[namecity]),state:getInputValue(input[namestate]),zip:getInputValue(input[namezip]),},phone:getInputValue(input[namephone]),email:getInputValue(input[nameemail]),},},self{letcustomersswitch(self.state.mode){|CreateBelt.Array.concat(state.customers,[|self.state.customer|])|UpdateBelt.Array.setExn(state.customers,Js.Array.findIndex(customercustomer.CustomerType.idself.state.customer.id,state.customers,),self.state.customer,);state.customers;};letjsoncustomers-DataBsJson.toJson;DataBsJson.setItem(customers,json);},);在这些更改之后我们的测试成功了PASSlib/es6/__tests__/Customers_test.bs.js Customer ✓ can create acustomer(5ms)Test Suites:1passed,1totalTests:1passed,1totalSnapshots:0totalTime:1.179s Ran all test suites.反思在本章中我们学习了如何使用 CommonJS 和 ES 模块格式设置bs-jest的基础知识。我们还了解到单元测试可以帮助我们编写更好的代码因为大部分情况下易于测试的代码也更好。我们将getCustomer重构为getCustomerById并将顾客数组移到该组件的状态中。由于我们在 Reason 中编写了单元测试编译器也会检查我们的测试。例如如果Customer_test.re使用getCustomer而我们在Customer.re中将getCustomerById更改为getCustomer我们会得到一个编译时错误Weve found a bugforyou!/__tests__/Customers_test.re45:9-2843|];44letcustomer:CustomerType.t45Customer.getCustomer(customers,2)|Belt.Option.getExn;46expect((customer.id,customer.name))|toEqual((2,Luise Grayson));47})The value getCustomer cant be foundinCustomerHint:Did you mean getCustomers?这意味着我们也无法编写某些单元测试。例如如果我们想要测试第五章中的Effective ML代码我们在那里使用类型系统来保证发票不会被打折两次测试甚至无法编译。多么美好。总结由于 Reason 的影响如此广泛有许多不同的学习方法。本书侧重于从前端开发人员的角度学习 Reason。我们学习了我们已经熟悉的技能和概念如使用 ReactJS 构建 Web 应用程序并探讨了如何在 Reason 中做同样的事情。在这个过程中我们了解了 Reason 的类型系统、工具链和生态系统。我相信 Reason 的未来是光明的。我们学到的许多技能都可以直接转移到针对本机平台。Reason 的前端故事目前比其本机故事更加完善但已经可以编译到 Web 和本机。而且从现在开始它只会变得更好。从我开始使用 Reason 的时候已经有了巨大的改进我很期待看到未来会有什么。希望这本书能引起您对 Reason、OCaml 和 ML 语言家族的兴趣。Reason 的类型系统经过数十年的工程技术。因此这本书没有涵盖的内容很多我自己也在不断学习。然而您现在应该已经建立了坚实的基础可以继续学习。我鼓励您通过在 Discord 频道上提问、撰写博客文章、指导他人、在聚会中分享您的学习经历等公开学习。非常感谢您能走到这一步并在 Discord 频道上见到您Reason Discord 频道discord.gg/reasonml