JavaScript和Node.js中的最佳测试技术


这是提供JavaScript和Node.js可靠性的综合指南。 这里收集了数十种最好的帖子,书籍和工具。

首先,处理作为任何应用程序基础的公认测试方法。 然后,您可以深入研究自己感兴趣的领域:前端和接口,后端,CI或以上所有内容。

目录内容



第0节:黄金法则


0.黄金法则:坚持精益测试


怎么办。 测试代码与所运行的代码不同。 使它尽可能简单,简短,没有抽象,单一,工作精益和节俭。 另一个人应该查看测试并立即了解他在做什么。

我们的负责人忙于生产代码,他们没有足够的空间来增加复杂性。 如果我们将新的复杂代码部分放到我们的可怜的头脑中,这将减慢整个团队在该任务上的工作,为此,我们正在对其进行测试。 实际上,因此,许多团队只是避开了测试。

测试是获得友好和微笑助手的机会,与他一起工作非常好,并且从小额投资中获得了丰厚的回报。 科学家认为,在我们的大脑中,有两种系统:一种用于不需要力气的动作(例如在空旷的道路上行驶),另一种用于需要意识的复杂操作(例如求解数学方程式)。 为第一个系统创建测试,以便在查看代码时获得与编辑HTML文档类似的简单感觉,而无需使用2X(17 × 24)解决方案2X(17 × 24)

可以通过仔细选择测试的方法,工具和目标来实现这一目标,从而使它们既经济又可以带来较大的投资回报率。 仅根据需要进行测试,并尽量保持灵活性。 有时为了快速和简单起见,甚至值得放弃一些测试并牺牲可靠性。



以下大多数建议均源于该原则。
准备好了吗

第1节。测试剖析


1.1每个测试的名称应由三部分组成


怎么办。 测试报告应指出该应用程序的当前版本是否满足那些不熟悉该代码的人的需求:参与DevOps工程师部署的测试人员以及您自己在两年内。 最好的是,测试以要求的语言报告信息,并且其名称由三部分组成:

  1. 到底在测试什么? 例如, ProductsService.addNewProduct方法。
  2. 在什么条件和情况下? 例如,价格不传递给该方法。
  3. 预期的结果是什么? 例如,新产品未被批准。

否则。 部署失败,称为“添加产品”的测试失败。 您了解什么是错误的吗?

注意事项 每章都有示例代码,有时还有插图。 见扰流板。

代码示例
正确的做法。 测试名称由三部分组成。

 //1. unit under test describe('Products Service', function() { describe('Add new product', function() { //2. scenario and 3. expectation 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根据AAA模式构建测试


怎么办。 每个测试应包括三个明显分开的部分:安排(准备),行为(行动)和声明(结果)。 坚持这样的结构可以确保您的代码阅读者不必使用大脑处理器来理解测试计划:

安排:根据测试方案使系统进入状态的所有代码。 这可能包括在测试设计器中创建模块的实例,将记录添加到数据库,创建存根而不是对象以及任何其他准备系统进行测试运行的代码。

行动:将代码执行作为测试的一部分。 通常只有一行。
声明:确保获得的价值符合期望。 通常只有一行。

否则。 您不仅会花费大量时间来处理主要代码,而且您的大脑也会因应做的简单工作-测试而膨胀。

代码示例
正确的做法。 根据AAA模式构造的测试。

 describe.skip('Customer classifier', () => { test('When customer spent more than 500$, should be classified as premium', () => { //Arrange const customerToClassify = {spent:505, joined: new Date(), id:1} const DBStub = sinon.stub(dataAccess, "getCustomer") .reply({id:1, classification: 'regular'}); //Act const receivedClassification = customerClassifier.classifyCustomer(customerToClassify); //Assert expect(receivedClassification).toMatch('premium'); }); }); 

反模式的一个例子。 一件一件没有一件分开的事更难解释。

 test('Should be classified as premium', () => { const customerToClassify = {spent:505, joined: new Date(), id:1} const DBStub = sinon.stub(dataAccess, "getCustomer") .reply({id:1, classification: 'regular'}); const receivedClassification = customerClassifier.classifyCustomer(customerToClassify); expect(receivedClassification).toMatch('premium'); }); 


1.3用产品的语言描述期望:以BDD的风格陈述


怎么办。 以声明式的方式进行编程测试,使用户可以立即了解其本质,而无需花费单个脑部处理器周期。 当您编写以条件逻辑包装的命令式代码时,读者必须付出很多努力。 从这个角度来看,您需要使用期望/应该并且不使用自定义代码,以声明性的BDD风格用类人语言描述期望。 如果在Chai和Jest中没有必要的断言(通常会重复执行),则可以扩展匹配器Jest为Chai编写自己的插件

否则。 该团队将编写更少的测试,并with .skip()装饰令人讨厌的测试。

代码示例
使用Mocha的示例

反模式的一个例子。 为了理解测试的本质,用户被迫通过相当长的强制性代码。

 it("When asking for an admin, ensure only ordered admins in results" , ()={ //assuming we've added here two admins "admin1", "admin2" and "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" , ()={ //assuming we've added here two admins const allAdmins = getUsers({adminOnly:true}); expect(allAdmins).to.include.ordered.members(["admin1" , "admin2"]) .but.not.include.ordered.members(["user1"]); }); 


1.4坚持黑盒测试:仅测试公共方法


怎么办。 测试内部将导致巨大的开销,并且几乎一无所获。 如果您的代码或API提供了正确的结果,是否值得花三个小时测试它如何在内部工作并随后支持这些脆弱的测试? 当检查公共行为时,同时隐式检查实现本身,只有在存在特定问题(例如,错误的输出)时,测试才会失败。 这种方法也称为行为测试。 另一方面,如果您正在测试内部结构(“白盒”方法),那么您将不着重于计划组件的输出,而是将重点放在小细节上,并且即使代码没问题,您的测试也可能由于代码的细微改动而中断,但是护送将占用更多资源。

否则。 您的测试就像一个男孩在喊“狼!” :大声报告误报(例如,由于私有变量名称的更改,测试失败)。 很快人们将开始忽略CI通知就不足为奇了,有一天他们会错过一个真正的bug ...

代码示例
反模式的一个例子。 没有充分的理由测试内部。

使用Mocha的示例

 class ProductService{ //this method is only used internally //Change this name will make the tests fail calculateVAT(priceWithoutVAT){ return {finalPrice: priceWithoutVAT * 1.2}; //Change the result format or key name above will make the tests fail } //public method 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 () => { //There's no requirement to allow users to calculate the VAT, only show the final price. Nevertheless we falsely insist here to test the class internals expect(new ProductService().calculateVATAdd(0).finalPrice).to.equal(0); }); 


1.5选择正确的模拟实现:避免使用伪造的对象,而使用存根和间谍


怎么办。 模拟实现(测试双打)是必不可少的,因为它们与应用程序的内部相关联,并且有些具有很大的价值( 刷新模仿的实现的内存:伪造的对象(模拟),存根和间谍对象(间谍)) ) 但是,并非所有技术都是等效的。 间谍程序和存根旨在测试需求,但具有不可避免的副作用-它们还会对内部产生轻微影响。 伪造的对象被设计为测试内部,这导致了巨大的开销,如第1.4章所述。

在使用模拟实现之前,请问自己一个最简单的问题:“我是否使用它来测试已出现在文档中或可能出现在需求中的功能?” 如果不是这样,就会出现白盒测试的痕迹。

例如,如果您想了解当支付服务不可用时应用程序的运行情况是否应该按其应有的方式运行,可以改成一个存根并返回“ No answer”以检查被测模块是否返回了正确的值。 因此,您可以在某些情况下检查应用程序的行为/响应/输出。 您还可以在间谍的帮助下确认,当服务不可用时,会发送信函,这也是行为测试,可以更好地反映在文档中,并带有要求(“如果无法保存付款信息,请发送信函”)。 同时,如果您提供了伪造的付款服务并确保使用正确的JS类型调用了该服务,则您的测试将针对与应用程序功能无关且可能经常更改的内部。

否则。 任何代码重构都涉及查找和更新代码中的所有伪造对象。 来自助理朋友的测试变成了负担。

代码示例
反模式的一个例子。 伪造物品是为了胆量。

使用Sinon的示例

 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 () => { //Assume we already added a product const dataAccessMock = sinon.mock(DAL); //hmmm BAD: testing the internals is actually our main goal here, not just a side-effect 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 () => { //Assume we already added here a product const spy = sinon.spy(Emailer.prototype, "sendEmail"); new ProductService().deletePrice(theProductWeJustAdded); //hmmm OK: we deal with internals? Yes, but as a side effect of testing the requirements (sending an email) }); 


1.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 @3e2ddsf . ##' 1 fdsfds . fds432 AAAA @3e2ddsf . ##' 1 fdsfds . fds432 AAAA

代码示例
反模式的一个例子。 由于使用了不真实的数据,测试套件成功运行。

使用Jest的示例

 const addProduct = (name, price) =>{ const productNameRegexNoSpace = /^\S*$/;//no white-space allowed if(!productNameRegexNoSpace.test(name)) return false;//this path never reached due to dull input //some logic here return true; }; test("Wrong: When adding new product with valid properties, get successful confirmation", async () => { //The string "Foo" which is used in all tests never triggers a false result const addProductResult = addProduct("Foo", 5); expect(addProductResult).to.be.true; //Positive-false: the operation succeeded because we never tried with long //product name including spaces }); 

正确的做法。 随机化现实输入。

 it("Better: When adding new valid product, get successful confirmation", async () => { const addProductResult = addProduct(faker.commerce.productName(), faker.random.number()); //Generated random input: {'Sleek Cotton Computer', 85481} expect(addProductResult).to.be.true; //Test failed, the random input triggered some path we never planned for. //We discovered a bug early! }); 


1.7使用基于属性的测试来验证多个输入组合


怎么办。 通常,对于每个测试,我们都会选择几个输入数据样本。 即使输入格式类似于真实数据(请参阅“请勿使用” foo”一章),我们也仅涵盖输入数据的几种组合(方法('', true, 1) ,方法("string" , false" , 0) )。但在操作中,可以用数千种不同的组合调用用五个参数调用的API,其中一种会导致进程崩溃模糊 )。如果编写一个自动发送1000个输入数据组合的测试,该怎么办?修复,代码以什么组合不能返回正确答案?我们对m所做的相同操作 todike基于性能测试:通过发送输入数据的所有可能组合成测试单元我们增加一个错误检测的机会,例如,我们有一个方法。 addNewProduct(id, name, isDiscount)支持他的库将调用此方法有多种组合。 (, , ) ,例如(1, "iPhone", false)(2, "Galaxy", true)等。您可以使用自己喜欢的测试运行器(Mocha,Jest (2, "Galaxy", true)基于属性进行测试等)和js-verifytestcheck之类的库(它有更好的文档)。 您还可以尝试快速检查库 ,该提供了附加功能,并由作者积极提供。

否则。 您无所顾忌地为测试选择了输入数据,该数据仅涵盖功能良好的代码执行路径。 不幸的是,这降低了测试作为检测错误的手段的有效性。

代码示例
正确的做法。 使用mocha-testcheck测试多种组合。

 require('mocha-testcheck').install(); const {expect} = require('chai'); const faker = require('faker'); describe('Product service', () => { describe('Adding new', () => { //this will run 100 times with different random properties 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'); }); }) }); 


1.8如有必要,请仅使用短镜头和内联镜头。


怎么办。 当您需要基于快照进行测试时 ,请仅使用简短的快照,而不使用所有多余的快照(例如,在3-7行中),包括它们作为测试的一部分( Inline Snapshot ),而不作为外部文件。 遵循此建议将使您的测试不言而喻,并且更加可靠。

另一方面,“经典快照”指南和工具促使我们在外部媒体上存储大文件(例如,用于渲染组件的标记或JSON API结果),并在每次运行测试时将结果与保存的版本进行比较。 可以说,它可以将我们的测试与包含3000个值的1000行隐式关联,该值是测试作者从未见过的,他从未想到过。 为什么这样不好? 因为测试失败有1000个原因。 甚至只有一行可以使快照无效,并且这种情况经常发生。 多少钱 在每个空格之后,注释或CSS或HTML中的微小变化。 此外,测试的名称不会告诉您有关失败的信息,因为它只会检查1000行没有更改,并且还鼓励测试的作者尽可能多地获取他无法分析和验证的长文档。 所有这些都是模糊而仓促的测试的症状,该测试没有明确的任务,并且试图实现太多目标。

值得注意的是,在很多情况下,使用长而外部的图像是可以接受的,例如,在确认方案时,而不是数据(提取值并关注字段)时,或者接收的文档很少更改时,都是可以接受的。

否则。 UI测试失败。 代码看起来不错,理想的像素显示在屏幕上,那么会发生什么呢? 您对快照的测试只是揭示了原始文档和刚收到的文档之间的区别-在标记中添加了一个空格字符...

代码示例
反模式的一个例子。 将测试与一些未知的2000行代码相关联。

 it('TestJavaScript.com is renderd correctly', () => { //Arrange //Act const receivedPage = renderer .create( <DisplayPage page = "http://www.testjavascript.com" > Test JavaScript < /DisplayPage>) .toJSON(); //Assert expect(receivedPage).toMatchSnapshot(); //We now implicitly maintain a 2000 lines long document //every additional line break or comment - will break this test }); 

正确的做法。 期望是可见的,并且是人们关注的焦点。

 it('When visiting TestJavaScript.com home page, a menu is displayed', () => { //Arrange //Act receivedPage tree = renderer .create( <DisplayPage page = "http://www.testjavascript.com" > Test JavaScript < /DisplayPage>) .toJSON(); //Assert const menu = receivedPage.content.menu; expect(menu).toMatchInlineSnapshot(` <ul> <li>Home</li> <li> About </li> <li> Contact </li> </ul> `); }); 


1.9避免使用全局测试平台和初始数据,将数据分别添加到每个测试中


怎么办。 根据黄金法则(第0章),每个测试都应在数据库中自己的行集中进行添加和工作,以避免绑定,并且使用户更容易理解测试。 实际上,测试人员通常在运行使用初始数据(种子)( 也称为“测试台” )填充数据库的测试之前违反此规则,以提高生产率。 , (. « »), . . , , (, ).

. , , , ? , , , .

反模式的一个例子。测试不是独立的,而是使用某种全局挂钩从数据库获取全局数据。

 before(() => { //adding sites and admins data to our DB. Where is the data? outside. At some external json or migration framework await DB.AddSeedDataFromJson('seed.json'); }); it("When updating site name, get successful confirmation", async () => { //I know that site name "portal" exists - I saw it in the seed files 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 () => { //I know that site name "portal" exists - I saw it in the seed files const siteToCheck = await SiteService.getSiteByName("Portal"); expect(siteToCheck.name).to.be.equal("Portal"); //Failure! The previous test change the name :[ }); 

正确的做法。您可以停留在测试中,每个测试仅适用于自己的数据。

 it("When updating site name, get successful confirmation", async () => { //test is adding a fresh new records and acting on the records only const siteUnderTest = await SiteService.addSite({ name: "siteForUpdateTest" }); const updateNameResult = await SiteService.changeName(siteUnderTest, "newName"); expect(updateNameResult).to.be(true); }); 


1.10 ,


. , - , try-catch-finally , . ( ), .

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

. (, CI-) , .

. , 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; //if this assertion fails, the tests results/reports will only show //that some value is null, there won't be a word about a missing Exception }); 

. , , , QA .

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


1.11


. :

  • smoke-,
  • IO-less,
  • , , ,
  • , pull request', .

, , , #cold #api #sanity. . , Mocha : mocha — grep 'sanity' .

. , , , , , , , .

. '#cold-test' (Cold=== , - , ).

 //this test is fast (no DB) and we're tagging it correspondingly //now the user/CI can run it frequently 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() { //code logic here }); }); }); 


1.12


. , Node.js . , Node.

TDD . , , , . -- , - . , , . , . , , , , (, ..).

. , .

2:


️2.1 :


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

: 2019- , TDD , , , . , , , . IoT-, Kafka RabbitMQ, , - . , , ? (, , ), , - .

( ) , , (« API, , !» (consumer-driven contracts)). , : , , , .

: TDD - . TDD , . , .

. ROI, Fuzz, , 10 .

. Cindy Sridharan 'Testing Microservices — the sane way'



一个例子:



2.2


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

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

. , , 20 %.

. Express API ( ).



2.3 , API


. , ( ). - , ! — , , . « -22 » : , , . (consumer-driven contracts) PACT : , … ! PACT — «», . PACT- — . , API CI, .

. — .

.



2.4


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

. Express- === .

. , Express-.

 //the middleware we want to test const unitUnderTest = require('./middleware') const httpMocks = require('node-mocks-http'); //Jest syntax, equivalent to describe() & it() in Mocha 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); }); 


2.5


. . CI- , . (, ), (, ), . Sonarqube (2600+ ) Code Climate (1500+ ). :: Keith Holliday

. , .

. CodeClimate, :



2.6 , Node


. , . ( ) . , - , ? ? , API 50 % ? , Netflix - ( Chaos Engineering ). : , . , Netflix, chaos monkey , , , , - ( Kubernetes kube-monkey , ). , . , , Node- , , v8 1,7 , UX , ? node-chaos (-), , Node.

. , production .

. Node-chaos , Node.js, .



2.7 ,


. ( 0), , , . , (seeds) ( « » ) . , (. « »), , . . , , (, ).

. , , , ? , , , .

. - .

 before(() => { //adding sites and admins data to our DB. Where is the data? outside. At some external json or migration framework await DB.AddSeedDataFromJson('seed.json'); }); it("When updating site name, get successful confirmation", async () => { //I know that site name "portal" exists - I saw it in the seed files 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 () => { //I know that site name "portal" exists - I saw it in the seed files const siteToCheck = await SiteService.getSiteByName("Portal"); expect(siteToCheck.name).to.be.equal("Portal"); //Failure! The previous test change the name :[ }); 

. , .

 it("When updating site name, get successful confirmation", async () => { //test is adding a fresh new records and acting on the records only const siteUnderTest = await SiteService.addSite({ name: "siteForUpdateTest" }); const updateNameResult = await SiteService.changeName(siteUnderTest, "newName"); expect(updateNameResult).to.be(true); }); 


3:


3.1。 UI


. , , , . , , ( HTML CSS) . , (, , , ), , , .

. 10 , 500 (100 = 1 ) - - .

. .

 test('When users-list is flagged to show only VIP, should display only VIP members', () => { // Arrange const allUsers = [ { id: 1, name: 'Yoni Goldberg', vip: false }, { id: 2, name: 'John Doe', vip: true } ]; // Act const { getAllByTestId } = render(<UsersList users={allUsers} showOnlyVIP={true}/>); // Assert - Extract the data from the UI first const allRenderedUsers = getAllByTestId('user').map(uiElement => uiElement.textContent); const allRealVIPUsers = allUsers.filter((user) => user.vip).map((user) => user.name); expect(allRenderedUsers).toEqual(allRealVIPUsers); //compare data with data, no UI here }); 

. UI .
 test('When flagging to show only VIP, should display only VIP members', () => { // Arrange const allUsers = [ {id: 1, name: 'Yoni Goldberg', vip: false }, {id: 2, name: 'John Doe', vip: true } ]; // Act const { getAllByTestId } = render(<UsersList users={allUsers} showOnlyVIP={true}/>); // Assert - Mix UI & data in assertion expect(getAllByTestId('user')).toEqual('[<li data-testid="user">John Doe</li>]'); }); 


3.2 HTML- ,


. HTML- , . , , CSS-. , 'test-id-submit-button'. . , , .

. , , . — , , Ajax . . , CSS 'thick-border' 'thin-border'

. , .

 // the markup code (part of React component) <b> <Badge pill className="fixed_badge" variant="dark"> <span data-testid="errorsLabel">{value}</span> <!-- note the attribute data-testid --> </Badge> </b> // this example is using react-testing-library test('Whenever no data is passed to metric, show 0 as default', () => { // Arrange const metricValue = undefined; // Act const { getByTestId } = render(<dashboardMetric value={undefined}/>); expect(getByTestId('errorsLabel')).text()).toBe("0"); }); 

. CSS-.

 <!-- the markup code (part of React component) --> <span id="metric" className="d-flex-column">{value}</span> <!-- what if the designer changes the classs? --> // this exammple is using enzyme test('Whenever no data is passed, error metric shows zero', () => { // ... expect(wrapper.find("[className='d-flex-column']").text()).toBe("0"); }); 


3.3


. , , . , , . , — - , (. « » ). (, ) , .

, : , . ( ) . , .

. , . ?

. .

 class Calendar extends React.Component { static defaultProps = {showFilters: false} render() { return ( <div> A filters panel with a button to hide/show filters <FiltersPanel showFilter={showFilters} title='Choose Filters'/> </div> ) } } //Examples use React & Enzyme test('Realistic approach: When clicked to show filters, filters are displayed', () => { // Arrange const wrapper = mount(<Calendar showFilters={false} />) // Act wrapper.find('button').simulate('click'); // Assert expect(wrapper.text().includes('Choose Filter')); // This is how the user will approach this element: by text }) 

. .

 test('Shallow/mocked approach: When clicked to show filters, filters are displayed', () => { // Arrange const wrapper = shallow(<Calendar showFilters={false} title='Choose Filter'/>) // Act wrapper.find('filtersPanel').instance().showFilters(); // Tap into the internals, bypass the UI and invoke a method. White-box approach // Assert expect(wrapper.find('Filter').props()).toEqual({title: 'Choose Filter'}); // what if we change the prop name or don't pass anything relevant? }) 


3.4 .


. (, ). (, setTimeOut ) , . (, Cypress cy.request('url') ), API, wait(expect(element)) @testing-library/DOM . , API, , . , , hurry-up the clock . — , , ( ). , , - npm- , , wait-for-expect .

. , . , . .

. E2E API (Cypress).

 // using Cypress cy.get('#show-products').click()// navigate cy.wait('@products')// wait for route to appear // this line will get executed only when the route is ready 

. , DOM- (@testing-library/dom).
 // @testing-library/dom test('movie title appears', async () => { // element is initially not present... // wait for appearance await wait(() => { expect(getByText('the lion king')).toBeInTheDocument() }) // wait for appearance and return the element const movie = await waitForElement(() => getByText('the lion king')) }) 

. .

 test('movie title appears', async () => { // element is initially not present... // custom wait logic (caution: simplistic, no timeout) const interval = setInterval(() => { const found = getByText('the lion king'); if(found){ clearInterval(interval); expect(getByText('the lion king')).toBeInTheDocument(); } }, 100); // wait for appearance and return the element const movie = await waitForElement(() => getByText('the lion king')) }) 


3.5.


. - , . , , . : pingdom , AWS CloudWatch gcp StackDriver , , SLA. , , (, lighthouse , pagespeed ), . — , : , (TTI) . , , , , , DOM, SSL . , CI, 247 CDN.

. , , , , - CDN.

. Lighthouse .



3.6 API


. ( 2), , , ( ). API (, Sinon , Test doubles ), API. . API , ( ). API, . , , API . , : .

. , API 100 , 20 .

. API-.

 // unit under test export default function ProductsList() { const [products, setProducts] = useState(false) const fetchProducts = async() => { const products = await axios.get('api/products') setProducts(products); } useEffect(() => { fetchProducts(); }, []); return products ? <div>{products}</div> : <div data-testid='no-products-message'>No products</div> } // test test('When no products exist, show the appropriate message', () => { // Arrange nock("api") .get(`/products`) .reply(404); // Act const {getByTestId} = render(<ProductsList/>); // Assert expect(getByTestId('no-products-message')).toBeTruthy(); }); 


3.7 ,


. E2E (end-to-end, ) UI (. 3.6). , , . , , - . , — (, ), . - , , UI- Cypress Pupeteer . , : 50 , , . 10 . , , , — . , .

. UI , , ( , UI) .

3.8


. , API , , . (before-all), - . , : . , . - API- . , . (, ), , , . , : , API (. 3.6).

. , 200 , 100 , 20 .

. (before-all), (before-each) (, Cypress).

Cypress .

 let authenticationToken; // happens before ALL tests run before(() => { cy.request('POST', 'http://localhost:3000/login', { username: Cypress.env('username'), password: Cypress.env('password'), }) .its('body') .then((responseFromLogin) => { authenticationToken = responseFromLogin.token; }) }) // happens before EACH test beforeEach(setUser => () { cy.visit('/home', { onBeforeLoad (win) { win.localStorage.setItem('token', JSON.stringify(authenticationToken)) }, }) }) 


3.9 smoke-,


. production- , , . , , , , . smoke- . production, , , . , smoke- , .

. , , production . /Payment.

. Smoke- .

 it('When doing smoke testing over all page, should load them all successfully', () => { // exemplified using Cypress but can be implemented easily // using any E2E suite cy.visit('https://mysite.com/home'); cy.contains('Home'); cy.contains('https://mysite.com/Login'); cy.contains('Login'); cy.contains('https://mysite.com/About'); cy.contains('About'); }) 


3.10


. , . «» , , , , . , ( ) , , -, , , . « », . . , Cucumber JavaScript . StoryBook UI- , (, , , ..) , . , , .

. , .

. cucumber-js.

 // this is how one can describe tests using cucumber: plain language that allows anyone to understand and collaborate Feature: Twitter new tweet I want to tweet something in Twitter @focus Scenario: Tweeting from the home page Given I open Twitter home Given I click on "New tweet" button Given I type "Hello followers!" in the textbox Given I click on "Submit" button Then I see message "Tweet saved" 

. Storybook , .



3.11


. , . , . , . , - . , . , , , . , - . UI « ». , (, wraith , PhantomCSS), . (, Applitools , Perci.io ) , , « » (, ), DOM/CSS, .

. , ( ) , ?

. : , .



. wraith UI.

 ​# Add as many domains as necessary. Key will act as a label​ domains: english: "http://www.mysite.com"​ ​# Type screen widths below, here are a couple of examples​ screen_widths: - 600​ - 768​ - 1024​ - 1280​ ​# Type page URL paths below, here are a couple of examples​ paths: about: path: /about selector: '.about'​ subscribe: selector: '.subscribe'​ path: /subscribe 


4:


4.1 (~80 %),


. — , . , . — (, ), . ? , 10-30 % . 100 % , . . , : Airbus, ; , 50 % . , , 80 % ( Fowler: «in the upper 80s or 90s» ), , , .

: (CI), ( Jest ) , . , . , ( ) — . , — , , . , .

. . , , . .

.



. ( Jest).



4.2 ,


. , . , , , , . , , - , . PricingCalculator , , , 10 000 … , , . , . 80- , . : , , , . , - .

. , , , .

. ? , QA . : , - . , - API .




4.3


. : 100 %, . 怎么会这样 , , , . . - : , , , .

, . JavaScript- Stryker :

  1. « ». , newOrder.price===0 newOrder.price!=0 . «» .
  2. , , : , . , , .

, , , .

. , 85- 85 % .

. 100 %, 0 %.

 function addNewOrder(newOrder) { logger.log(`Adding new order ${newOrder}`); DB.save(newOrder); Mailer.sendMail(newOrder.assignee, `A new order was places ${newOrder}`); return {approved: true}; } it("Test addNewOrder, don't use such test names", () => { addNewOrder({asignee: "John@mailer.com",price: 120}); });//Triggers 100% code coverage, but it doesn't check anything 

. Stryker reports, , ().



4.4 -


. ESLint. , eslint-plugin-mocha , ( describe() ), , . eslint-plugin-jest , ( ).

. 90- , , , . , .

. , , .

 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 }); 


5: CI


5.1 ,


. — . , . , ( !). , . ( ESLint standard Airbnb ), . , eslint-plugin-chai-expect , . Eslint-plugin-promise ( ). Eslint-plugin-security , DOS-. eslint-plugin-you-dont-need-lodash-underscore , , V8, , Lodash._map(…) .

. , , . 这是怎么回事? , , . . , , .

. , . , ESLint production-.



5.2


. CI , , ..? , . 怎么了 : (1) -> (2) -> (3) . , , .

, , , , - .

CI- ( , CircleCI local CLI ) . , wallaby , ( ) . npm- package.json, , (, , , ). (non-zero exit code) concurrently . — , npm run quality . githook ( husky ).

. , .

. Npm-, , , .

 "scripts": { "inspect:sanity-testing": "mocha **/**--test.js --grep \"sanity\"", "inspect:lint": "eslint .", "inspect:vulnerabilities": "npm audit", "inspect:license": "license-checker --failOn GPLv2", "inspect:complexity": "plato .", "inspect:all": "concurrently -c \"bgBlue.bold,bgMagenta.bold,yellow\" \"npm:inspect:quick-testing\" \"npm:inspect:lint\" \"npm:inspect:vulnerabilities\" \"npm:inspect:license\"" }, "husky": { "hooks": { "precommit": "npm run inspect:all", "prepush": "npm run inspect:all" } } 


5.3 production-


. — CI-. . — Docker-compose . (, ) production-. AWS Local AWS-. , serverless AWS SAM Faas-.

Kubernetes CI-, . , « Kubernetes» Minikube MicroK8s , , . « Kubernetes»: CI- (, Codefresh ) Kubernetes-, CI- ; .

. .

: CI-, Kubernetes- (Dynamic-environments Kubernetes )

 deploy: stage: deploy image: registry.gitlab.com/gitlab-examples/kubernetes-deploy script: - ./configureCluster.sh $KUBE_CA_PEM_FILE $KUBE_URL $KUBE_TOKEN - kubectl create ns $NAMESPACE - kubectl create secret -n $NAMESPACE docker-registry gitlab-registry --docker-server="$CI_REGISTRY" --docker-username="$CI_REGISTRY_USER" --docker-password="$CI_REGISTRY_PASSWORD" --docker-email="$GITLAB_USER_EMAIL" - mkdir .generated - echo "$CI_BUILD_REF_NAME-$CI_BUILD_REF" - sed -e "s/TAG/$CI_BUILD_REF_NAME-$CI_BUILD_REF/g" templates/deals.yaml | tee ".generated/deals.yaml" - kubectl apply --namespace $NAMESPACE -f .generated/deals.yaml - kubectl apply --namespace $NAMESPACE -f templates/my-sock-shop.yaml environment: name: test-for-ci 


5.4


. , , . , 500 , , . , CI- ( Jest , AVA Mocha ) , . CI- (!), . , CLI , , .

. — , .

. Mocha parallel Jest Mocha ( JavaScript Test-Runners Benchmark )



5.5


. , . 10 ? CI- npm- license check plagiarism check ( ), , , Stackoveflow .

. , , .

.
 //install license-checker in your CI environment or also locally npm install -g license-checker //ask it to scan all licenses and fail with exit code other than 0 if it found unauthorized license. The CI system should catch this failure and stop the build license-checker --summary --failOn BSD 



5.6


. , Express, . npm audit , snyk ( ). CI .

. . .

: NPM Audit



5.7


. package-lock.json Yarn npm ( ): . npm install npm update , . , — . , package.json ncu .

, . :

  • CI , , npm outdated npm-check-updates (ncu). .
  • , pull request' .

: ? , ( , eslint-scope ). « »: latest , , (, 1.3.1, — 1.3.8).

. , .

: , , ncu CI-.



5.8 CI-, Node


. , Node . , Node.

  1. . , Jenkins .
  2. Docker.
  3. , . . smoke-, (, , ) .
  4. , , , , , .
  5. , . , -. - ( ).
  6. . .
  7. , , .
  8. (, Docker-).
  9. , . node_modules .

. , .

5.9 : CI-, Node


. , , . Node, CI . , MySQL, Postgres. CI- «», MySQl, Postgres Node. , - (, ). CI, , .

. - ?

: Travis ( CI) Node.

 language: node_js node_js: - "7" - "6" - "5" - "4" install: - npm install script: - npm run test 

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


All Articles