测试Node.js项目。 第1部分。测试解剖结构和测试类型

该材料的作者(我们今天出版的翻译的第一部分)说,作为Node.js的独立顾问,他每年分析10多个项目。 他的客户很有道理,要求他特别注意测试。 几个月前,他开始就有价值的测试技术和遇到的错误做笔记。 结果是包含三打建议的材料。

图片

特别是,它将着重于选择适合特定情况的测试类型,适当的设计,评估其有效性以及需要将其放置在CI / CD链中的确切位置。 这里的一些示例使用Jest进行说明,某些示例使用Mocha。 该材料主要不是针对工具,而是测试方法。

测试Node.js项目。 第2部分。测试性能评估,持续集成和代码质量分析

▍0。 黄金法则:测试应该非常简单明了


您是否认识任何人-朋友,家庭成员,电影的英雄,他们总是心情愉快,随时准备提供帮助,而无需任何回报? 那就是应该设计好的测试的方式。 他们应该简单,有益并且唤起积极的情绪。 这可以通过仔细选择测试方法,工具和目标来实现。 那些使用证明在准备和进行测试上花费时间和精力并同时产生良好结果的人。 您只需要测试需要测试的内容,就应该努力确保测试既简单又灵活,有时您可以拒绝某些测试,这意味着以其简单性和开发速度牺牲了项目的可靠性。

测试不应视为正常的应用程序代码。 事实是,在任何情况下,从事项目开发的典型团队都会尽一切可能使项目保持工作状态,也就是说,努力使商业产品按其用户期望的方式工作。 结果,这样的团队可能感觉不太好,必须支持一组测试所代表的另一个复杂的“项目”。 如果对主代码的测试不断增长,越来越多地关注自身并成为引起持续关注的原因,那么对它们的工作要么被放弃,要么试图保持一个体面的水平,将给他们太多的时间和精力,这将减慢主项目的工作。

因此,测试代码应尽可能简单,并具有最少数量的依赖关系和抽象级别。 测试的外观应一目了然。 我们将在此处考虑的大多数建议均源于该原则。

第一节。测试剖析


▍1。 设计测试,以便报告可以告诉您正在测试什么,在什么情况下以及对测试的期望


推荐建议


测试报告应指出应用程序的当前版本是否满足其要求。 这应该以那些不必熟悉应用程序代码的人可以理解的形式完成。 这可能是测试人员,正在部署项目的DevOps专家,或者是开发人员本人,他们在编写代码后的某个时间都在看项目。 如果在编写测试时专注于产品需求,则可以实现这一目标。 通过这种方法,可以想象测试的结构包括三个部分:

  1. 到底在测试什么? 例如, ProductsService.addNewProduct方法。
  2. 在什么情况下以及什么情况下进行测试? 例如,在商品价格尚未传递给该方法的情况下,检查系统的反应。
  3. 预期的测试结果是什么? 例如,在类似情况下,系统拒绝确认向其添加新产品。

偏离建议的后果


假设无法部署该系统,并且从测试报告中您只能发现它没有通过名为Add product的测试,该测试会检查是否向其中添加了某种产品。 这会提供有关到底出了什么问题的信息吗?

正确的方法


测试信息包括三部分信息。

 //1.   describe('Products Service', function() { //2.  describe('Add new product', function() {   // 3. ,       it('When no price is specified, then the product status is pending approval', ()=> {     const newProduct = new ProductService().add(...);     expect(newProduct.status).to.equal('pendingApproval');   }); }); }); 

正确的方法


测试报告类似于包含产品要求声明的文档。

这是它在不同级别上的外观。


产品需求文档,测试命名,测试结果

  1. 具有产品需求的文档实际上可以是特殊文档,也可以以电子邮件之类的形式存在。
  2. 在命名测试,描述测试目的,场景和预期结果时,您需要遵循用于制定产品要求的语言。 这将帮助您比较测试代码和产品要求。
  3. 即使对于不熟悉应用程序代码或已完全忘记它的人,测试结果也应该清晰可见。 这些测试人员,DevOps专家,开发人员在编写代码几个月后便恢复使用该代码。

▍2。 用产品语言描述您对测试的期望:使用BDD样式的语句


推荐建议


以声明式的方式开发测试,使与他们合作的人员能够立即掌握其本质。 如果使用命令式方法编写测试,则它们将充满条件构造,这会使它们的理解大大复杂化。 遵循这一原则,应以与普通语言相近的语言来描述期望。 声明性BDD样式使用expectshould构造,而不是自己设计的一些特殊代码。 如果Chai或Jest中没有必要的语句,并且事实证明经常需要这样的语句,请考虑向Jest添加新的“检查”为Chai编写自己的插件

偏离建议的后果


如果您不遵循上述建议,最终将导致开发团队的成员编写更少的测试,并使用.skip()方法跳过烦人的检查。

错误的方法


该测试的读者仅需完全回顾相当长的命令性代码,才能了解测试中正在测试的内容。

 it("When asking for an admin, ensure only ordered admins in results" , ()={   //,       — "admin1"  "admin2",   "user1"   const allAdmins = getUsers({adminOnly:true});   const admin1Found, adming2Found = false;   allAdmins.forEach(aSingleUser => {       if(aSingleUser === "user1"){           assert.notEqual(aSingleUser, "user1", "A user was found and not admin");       }       if(aSingleUser==="admin1"){           admin1Found = true;       }       if(aSingleUser==="admin2"){           admin2Found = true;       }   });   if(!admin1Found || !admin2Found ){       throw new Error("Not all admins were returned");   } }); 

正确的方法


您可以一目了然地理解此测试。

 it("When asking for an admin, ensure only ordered admins in results" , ()={   //,        const allAdmins = getUsers({adminOnly:true});     expect(allAdmins).to.include.ordered.members(["admin1" , "admin2"]) .but.not.include.ordered.members(["user1"]); }); 

▍3。 使用特殊插件执行测试代码整理


推荐建议


有一组ESLint插件,专门用于分析测试代码和查找此类代码中的问题。 例如,如果测试是在全局级别编写的(并且不是describe()的后代),或者如果测试最终被跳过 ,则eslint-plugin-mocha插件-将发出警告,这可能会给所有测试通过的错误希望。 eslint-plugin-jest插件的工作方式类似,例如,警告没有语句的测试,即关于不检查任何内容的测试的警告。

偏离建议的后果


开发人员将很高兴看到测试中覆盖了90%的代码,并且100%的测试成功通过了。 但是,它只会保持这种状态,直到事实证明许多测试实际上不检查任何内容,并且仅跳过了一些测试脚本。 只能希望没有人会在生产中部署经过这种“测试”的项目。

错误的方法


测试场景中充满了错误,幸运的是,可以使用短绒检测到错误。

 describe("Too short description", () => { const userToken = userService.getDefaultToken() // *error:no-setup-in-describe, use hooks (sparingly) instead it("Some description", () => {});//* error: valid-test-description. Must include the word "Should" + at least 5 words }); it.skip("Test name", () => {// *error:no-skipped-tests, error:error:no-global-tests. Put tests only under describe or suite expect("somevalue"); // error:no-assert }); it("Test name", () => {*//error:no-identical-title. Assign unique titles to tests }); 

▍4。 坚持黑匣子方法-仅测试公共方法


推荐建议


测试一些内部代码机制意味着大大增加了开发人员的负担,并且几乎没有任何好处。 如果某个API产生正确的结果,是否值得花几个小时测试其内部机制,然后仍然支持这些测试,而这些测试很容易就被破坏了? 测试公开可用的方法时,尽管隐式地验证了它们的内部实现,但也已对其进行了验证。 如果系统出现问题,这种测试将产生错误,从而导致发布错误的数据。 这种方法也称为“行为测试”。 另一方面,通过测试某个API的内部机制(即使用“白盒”技术),开发人员将重点放在实现的小细节上,而不是代码的最终结果上。 例如,即使系统继续产生正确的结果,检查这种细微差别的测试也可能开始产生错误,例如,在进行少量代码重构之后。 结果,这种方法大大增加了与测试代码的支持相关联的程序员的负担。

偏离建议的后果


试图捕捉某个系统内部机制的测试的行为就像一个寓言中的牧羊男孩,他用“求救! 狼!”附近没有狼时。 人们急忙求助,才发现自己被欺骗了。 当狼真的出现时,没有人来营救。 例如,在某些内部变量的名称更改的情况下,此类测试会产生假阳性结果。 结果,进行这些测试的人很快开始忽略他们的“尖叫”就不足为奇了,这最终导致了一个事实,那就是一旦发现真正的严重错误就不会被察觉。

错误的方法


该测试无需特殊原因即可测试类的内部机制。

 class ProductService{ //      //     ,      calculateVAT(priceWithoutVAT){   return {finalPrice: priceWithoutVAT * 1.2};   //           } //  getPrice(productId){   const desiredProduct= DB.getProduct(productId);   finalPrice = this.calculateVATAdd(desiredProduct.price).finalPrice; } } it("White-box test: When the internal methods get 0 vat, it return 0 response", async () => {   //       VAT,      .  ,   ,        expect(new ProductService().calculateVATAdd(0).finalPrice).to.equal(0); }); 

5英镑 选择适当的备份对象:避免暴民,更喜欢存根和间谍


推荐建议


当测试是必不可少的行为时,使用测试加倍,因为它们与应用程序的内部机制相关联。 没有其中的一些,那就根本不可能。 这是有关此主题的有用材料。 但是,使用此类对象的各种方法不能称为等效方法。 因此,其中的一些(存根(stub)和间谍(spy))旨在测试产品的需求,但是,由于不可避免的副作用,它们被迫略微影响该产品的内部机制。 另一方面,模拟的目的是测试项目的内部机制。 因此,它们的使用给程序员带来了巨大的不必要负担,我们在上面已经谈到过,在编写测试时要遵循“黑匣子”方法。

在使用重复的对象之前,请问自己一个简单的问题:“我是否使用它们来测试所描述的功能,或者可以在项目的技术要求中对其进行描述?”。 如果对这个问题的回答是否定的,则可能意味着您将要使用“白盒”方法来测试产品,我们已经谈到了缺点。

例如,如果您想确定您的应用程序在无法使用支付服务的情况下是否可以正常工作,则可以停止该服务并使该应用程序收到表明无人回答的内容。 这将允许您检查系统对类似情况的反应,以了解系统是否正常运行。 在这种测试过程中,将检查在某些条件下应用程序的行为或响应或结果。 在这种情况下,您可以使用间谍检查在检测到付款服务下降后是否发送了某些电子邮件。 同样,这将是对系统在特定情况下的行为的检查,可以确定地将其记录在其技术要求中,例如,格式如下:“如果付款未通过,请向管理员发送电子邮件”。 另一方面,如果您使用模拟对象表示付款服务,并在访问服务时将其期望值转移给他,请检查该操作,那么我们将讨论测试与应用程序功能不直接相关的内部机制,并且也许可以经常改变。

偏离建议的后果


使用任何代码重构,您都必须搜索所有moki,重构及其代码。 结果,测试支持将变成沉重的负担,使他们成为开发人员的敌人,而不是他的朋友。

错误的方法


此示例显示了一个模拟对象,该对象专注于测试应用程序的内部机制。

 it("When a valid product is about to be deleted, ensure data access DAL was called once, with the right product and right config", async () => {   //,        const dataAccessMock = sinon.mock(DAL);   // ,           dataAccessMock.expects("deleteProduct").once().withArgs(DBConfig, theProductWeJustAdded, true, false);   new ProductService().deletePrice(theProductWeJustAdded);   mock.verify(); }); 

正确的方法


间谍的目的是测试系统是否符合其要求,但副作用是不可避免地会影响系统的内部机制。

 it("When a valid product is about to be deleted, ensure an email is sent", async () => {   //,        const spy = sinon.spy(Emailer.prototype, "sendEmail");   new ProductService().deletePrice(theProductWeJustAdded);   //  .       ? ,               (  ) }); 

▍6。 在测试期间,请使用真实的输入,而不仅限于“ foo”之类的内容


推荐建议


生产中的错误通常表现在非常特殊甚至令人惊讶的情况组合中。 这意味着测试期间使用的输入数据越接近现实,就越早发现错误。 用于生成类似于真实的专业库(例如Faker)的数据 。 例如,此类库生成随机但真实的电话号码,用户名,银行卡号,公司名称,甚至“ lorem ipsum”文本。 此外,考虑在测试中使用生产环境中的数据。 如果您想将此类测试提高到更高的水平,请参考我们关于基于属性的测试的下一个建议。

偏离建议的后果


在项目开发过程中进行测试时,仅当使用不真实的数据(例如“ foo”行)执行所有测试时,才能通过所有测试。 但是在生产中,如果黑客给她类似@3e2ddsf . ##' 1 fdsfds . fds432 AAAA ,则系统将失败@3e2ddsf . ##' 1 fdsfds . fds432 AAAA @3e2ddsf . ##' 1 fdsfds . fds432 AAAA @3e2ddsf . ##' 1 fdsfds . fds432 AAAA

错误的方法


系统仅通过使用不切实际的数据才能成功通过这些测试。

 const addProduct = (name, price) =>{ const productNameRegexNoSpace = /^\S*$/;//  if(!productNameRegexNoSpace.test(name))   return false;// , -   ,  .     //  -    return true; }; it("Wrong: When adding new product with valid properties, get successful confirmation", async () => {   // "Foo",    ,    ,    false   const addProductResult = addProduct("Foo", 5);   expect(addProductResult).to.be.true;   // :     - ,     //         }); 

正确的方法


它使用类似于真实数据的随机数据。

 it("Better: When adding new valid product, get successful confirmation", async () => {   const addProductResult = addProduct(faker.commerce.productName(), faker.random.number());   //   : {'Sleek Cotton Computer',  85481}   expect(addProductResult).to.be.true;   //  ,         ,    .   //     ,    ! }); 

▍7。 使用基于属性的测试使用多个输入组合测试系统


推荐建议


通常,测试使用少量输入数据。 即使它们类似于真实数据(我们在上一节中已经讨论过),此类测试也仅涵盖了非常有限数量的被调查实体的可能输入组合。 例如,它可能看起来像这样: (method('', true, 1), method("string" , false" , 0)) 。问题是在生产API中,使用五个参数调用它,可以得到输入数千种不同组合的变体,其中一种可能会导致失败( 在这里应该召回模糊测试)。如果您可以编写一个测试来自动检查某方法的1000种组合,并找出哪种方法,该怎么办?该方法是否响应不正确?基于属性验证的测试正是 我们在这种情况下是有用的。也就是说,在这个测试模块检查的过程中,输入数据,这增加了找到一些错误的概率的所有可能的组合调用它。假设我们有一个方法addNewProduct(id, name, isDiscount)和库执行测试时,可以使用数字,字符串和逻辑类型的许多参数组合来调用它,例如- (1, "iPhone", false)(2, "Galaxy", true) 。 可以使用常规的测试执行环境(Mocha,Jest等)并使用诸如js-verifytestcheck之类的专用库(该库具有很好的文档),基于属性验证来进行测试。

偏离建议的后果


开发人员在不知不觉中选择了这样的测试数据,该测试数据仅覆盖代码正常工作的那些部分。 不幸的是,这降低了测试作为检测错误的手段的有效性。

正确的方法


使用mocha-testcheck库测试许多输入选项。

 require('mocha-testcheck').install(); const {expect} = require('chai'); const faker = require('faker'); describe('Product service', () => { describe('Adding new', () => {   //  100         check.it('Add new product with random yet valid properties, always successful',     gen.int, gen.string, (id, name) => {       expect(addNewProduct(id, name).status).to.equal('approved');     }); }) }); 

8英镑 力争使测试代码自成体系,最大程度地减少外部辅助和抽象


推荐建议


现在很明显,我致力于进行极其简单的测试。 事实是,否则某个项目的开发团队实际上必须处理另一个项目。 为了理解他的代码,他们必须花费宝贵的时间,而他们没有那么多。 关于这种现象的文章写得非常好:“高质量的生产代码是经过深思熟虑的代码,而高质量的测试代码是完全可以理解的代码……编写测试时,请考虑谁会看到他显示的错误消息。 为了理解错误的原因,该人员不希望读取整个测试套件的代码或用于测试的实用程序的继承树的代码。”

为了使读者在不离开测试代码的情况下理解测试,请在执行测试时最大程度地减少使用实用程序,挂钩或任何外部机制。 如果要执行此操作,则需要过于频繁地复制和粘贴代码,则可以停止使用一种外部辅助机制,使用该机制不会违反测试的可理解性。 但是,如果此类机制的数量增加,则测试代码将失去可理解性。

偏离建议的后果


, 4 , 2 , ? ! , , .


. ?

 test("When getting orders report, get the existing orders", () => {   const queryObject = QueryHelpers.getQueryObject(config.DBInstanceURL);   const reportConfiguration = ReportHelpers.getReportConfig();//   ?        userHelpers.prepareQueryPermissions(reportConfiguration);//  ?         const result = queryObject.query(reportConfiguration);   assertThatReportIsValid();//  ,           -    expect(result).to.be.an('array').that.does.include({id:1, productd:2, orderStatus:"approved"});   //      ?        }) 

正确的方法


, .

 it("When getting orders report, get the existing orders", () => {   // ,           const orderWeJustAdded = ordersTestHelpers.addRandomNewOrder();   const queryObject = newQueryObject(config.DBInstanceURL, queryOptions.deep, useCache:false);   const result = queryObject.query(config.adminUserToken, reports.orders, pageSize:200);   expect(result).to.be.an('array').that.does.include(orderWeJustAdded); }) 

▍9. :


推荐建议


, , , , . . , ( ) . — , ( , ). — , , , , . , , . — , , , , ( , , , ).

偏离建议的后果


, . . . . , , , .


. , .

 before(() => { //       .   ? - ,  -   json-. await DB.AddSeedDataFromJson('seed.json'); }); it("When updating site name, get successful confirmation", async () => { // ,  ,  "Portal", ,           const siteToUpdate = await SiteService.getSiteByName("Portal"); const updateNameResult = await SiteService.changeName(siteToUpdate, "newName"); expect(updateNameResult).to.be(true); }); it("When querying by site name, get the right site", async () => { // ,  ,  "Portal", ,           const siteToCheck = await SiteService.getSiteByName("Portal"); expect(siteToCheck.name).to.be.equal("Portal"); //!      :[ }); 

正确的方法


, , .

 it("When updating site name, get successful confirmation", async () => { //           const siteUnderTest = await SiteService.addSite({   name: "siteForUpdateTest" }); const updateNameResult = await SiteService.changeName(siteUnderTest, "newName"); expect(updateNameResult).to.be(true); }); 

▍10. , . expect


推荐建议


, , try-catch-finally catch . , , , .

Chai, expect(method).to.throw . Jest: expect(method).toThrow() . , . , , .

偏离建议的后果


, , , .


, try-catch .

 it("When no product name, it throws error 400", async() => { let errorWeExceptFor = null; try { const result = await addNewProduct({name:'nest'});} catch (error) { expect(error.code).to.equal('InvalidInput'); errorWeExceptFor = error; } expect(errorWeExceptFor).not.to.be.null; //    ,         //  ,     null,       }); 

正确的方法


expect , , .

 it.only("When no product name, it throws error 400", async() => { expect(addNewProduct)).to.eventually.throw(AppError).with.property('code', "InvalidInput"); }); 

▍11. ,


推荐建议


. , (smoke test), -, , . - . , , , #cold , #api , #sanity . . , Mocha -g ( --grep ).

偏离建议的后果


, , , , , , . .

正确的方法


#cold-test , . , -, , — .

 //    ( ,    ),     // ,        //   describe('Order service', function() { describe('Add new order #cold-test #sanity', function() {   it('Scenario - no currency was supplied. Expectation - Use the default currency #sanity', function() {     //-    }); }); }); 

▍12.


推荐建议


, Node.js-. , , Node.js .

TDD — , , . , . , , Red-Green-Refactor . , - , , , , , . , . ( — , , ).

偏离建议的后果


— , . .

2.


▍13. ,


推荐建议


, , 10 , . , . , . , (, ), , , , ? - ?

, . , 2019 , , TDD, — , . , , , . , IoT-, , , - Kafka RabbitMQ, . - , , , . , , , , ? (, , Alexa) , , .

, ( ). , , , , , , . , , - API — Consumer-Driven Contracts . , , , , . , , , , , . , , .

, TDD . , TDD , . , , , .

偏离建议的后果


— ( ), .

正确的方法


. . , , Node.js, .

▍14. ,


推荐建议


. — . , , . , , - , , - ? , . , : TDD, — .

«». API, - , (, , , , , ). , , , (, ). , , , , , , .

偏离建议的后果


, , , , 20.

正确的方法


supertest , API, Express, .


API, Express

▍15. , API, Consumer-Driven Contracts


推荐建议


, , , , . , , - , , , - . «-22» : . , , . , Consumer-Driven Contracts PACT .

. . PACT , ( «»). , , PACT, , , . , , , , .

偏离建议的后果


.

正确的方法



Consumer-Driven Contracts

, , B , . B .

▍16.


推荐建议


(middleware) - , , - , Express-. . , , . , , JS- {req,res} . , «» (, Sinon ) , {req,res} . , , , . node-mock-http , , , . , , HTTP-, -.

偏离建议的后果


Express .

正确的方法


Express-.

 // ,     const unitUnderTest = require('./middleware') const httpMocks = require('node-mocks-http'); //  Jest,     Mocha    describe()  it() test('A request without authentication header, should return http status 403', () => { const request = httpMocks.createRequest({   method: 'GET',   url: '/user/42',   headers: {     authentication: ''   } }); const response = httpMocks.createResponse(); unitUnderTest(request, response); expect(response.statusCode).toBe(403); }); 

▍17.


推荐建议


, , , , , , . , , - . , , , ( , , ), , ( — ) . , : SonarQube ( 2600 GitHub) Code Climate ( 1500 ).

偏离建议的后果


, , . . , .

正确的方法


Code Climate.


Code Climate

▍18. , Node.js


推荐建议


, , . , , , - , . , - , , , , ? , , ? , API ?

Netflix - . , , , , . , - — Chaos Monkey . , , , . Kubernetes — kube-monkey . , Node.js? , , , V8 1.7 . . node-chaos , -.

偏离建议的后果


, , , .

正确的方法


npm- chaos-monkey , Node.js.


chaos-monkey

总结


, Node.js-. , . .

亲爱的读者们! - ?

Source: https://habr.com/ru/post/zh-CN435462/


All Articles