Melhores técnicas de teste em JavaScript e Node.js


Este é um guia abrangente para fornecer confiabilidade em JavaScript e Node.js. Aqui são coletadas dezenas dos melhores posts, livros e ferramentas.

Primeiro, lide com métodos de teste geralmente aceitos e subjacentes a qualquer aplicativo. E então você pode se aprofundar na área de seu interesse: front-end e interfaces, back-end, IC ou todos os itens acima.

Conteúdo



Seção 0. Regra de Ouro


0. Regra de Ouro: Atenha-se aos Testes Lean


O que fazer O código de teste é diferente do que entra em operação. Torne o mais simples possível, curto, livre de abstrações, único, maravilhoso no trabalho e econômico. Outra pessoa deve olhar para o teste e entender imediatamente o que está fazendo.

Nossas cabeças estão ocupadas com o código de produção, elas não têm espaço livre para complexidade adicional. Se colocarmos uma nova parte do código complexo em nossa mente fraca, isso atrasará o trabalho de toda a equipe na tarefa, pelo que estamos testando. De fato, por causa disso, muitas equipes simplesmente evitam os testes.

Testes - é uma oportunidade de obter um assistente amigável e sorridente, com quem é muito bom trabalhar e que oferece um retorno enorme sobre pequenos investimentos. Os cientistas acreditam que em nosso cérebro existem dois sistemas: um para ações que não exigem esforço, como dirigir em uma estrada vazia, e o segundo para operações complexas que exigem consciência, como resolver equações matemáticas. Crie seus testes para o primeiro sistema, para que, ao examinar o código, tenha uma sensação de simplicidade comparável à edição de um documento HTML, e não com uma solução 2X(17 × 24) .

Isso pode ser alcançado através da seleção cuidadosa de métodos, ferramentas e objetivos para teste, para que sejam econômicos e ofereçam um ROI grande. Teste apenas o necessário, tente ser flexível. Às vezes, vale a pena até mesmo descartar alguns testes e sacrificar a confiabilidade por uma questão de velocidade e simplicidade.



A maioria das recomendações abaixo é derivada desse princípio.
Você está pronta?

Seção 1. Anatomia do teste


1.1 O nome de cada teste deve consistir em três partes


O que fazer O relatório de teste deve indicar se a revisão atual do aplicativo atende aos requisitos das pessoas que não estão familiarizadas com o código: testadores envolvidos na implantação dos engenheiros do DevOps, além de você mesmo em dois anos. Será melhor que os testes relatem informações no idioma dos requisitos e seus nomes consistam em três partes:

  1. O que exatamente está sendo testado? Por exemplo, o método ProductsService.addNewProduct .
  2. Sob quais condições e cenários? Por exemplo, o preço não é passado para o método
  3. Qual é o resultado esperado? Por exemplo, um novo produto não é aprovado.

Caso contrário. A implantação falha, o teste chamado "Adicionar produto" falha. Você entende o que exatamente funciona errado?

Nota Cada capítulo tem um código de exemplo e, às vezes, uma ilustração. Veja spoilers.

Exemplos de código
Como fazer certo. O nome do teste consiste em três partes.

 //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 Estruture os testes de acordo com o padrão AAA


O que fazer Cada teste deve consistir em três seções claramente separadas: organizar (preparação), ato (ação) e afirmar (resultado). A adesão a essa estrutura garante que o leitor do seu código não precise usar o processador cerebral para entender o plano de teste:

Organizar: todo o código que leva o sistema a um estado de acordo com o cenário de teste. Isso pode incluir a criação de uma instância do módulo no designer de teste, a adição de registros ao banco de dados, a criação de stubs em vez de objetos e qualquer outro código que prepare o sistema para a execução do teste.

Ato: execução de código como parte de um teste. Geralmente apenas uma linha.
Afirmar: verifique se o valor obtido atende às expectativas. Geralmente apenas uma linha.

Caso contrário. Você não passará apenas longas horas trabalhando com o código principal, mas seu cérebro também inchará do que deveria ser um trabalho simples - dos testes.

Exemplos de código
Como fazer certo. Um teste estruturado de acordo com o padrão 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'); }); }); 

Um exemplo de antipadrão. Nenhuma separação, em uma única peça, é mais difícil de interpretar.

 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 Descrever as expectativas no idioma do produto: estado no estilo do BDD


O que fazer Testes de programação em estilo declarativo permitem que o usuário entenda imediatamente a essência sem gastar um único ciclo de processador cerebral. Quando você escreve um código imperativo empacotado em lógica condicional, o leitor precisa fazer muito esforço. Desse ponto de vista, é necessário descrever as expectativas em uma linguagem humana em um estilo declarativo de BDD usando expect / should e não usando código personalizado. Se em Chai e Jest não houver afirmação necessária, que é frequentemente repetida, você poderá expandir o matcher Jest ou escrever seu próprio plugin para o Chai .

Caso contrário. A equipe escreverá menos testes e decorará testes irritantes with .skip() .

Exemplos de código
Um exemplo usando o Mocha .

Um exemplo de antipadrão. Para entender a essência do teste, o usuário é forçado a passar por um código imperativo bastante longo.

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

Como fazer certo. A leitura deste teste declarativo é simples.

 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 Aderir ao teste de caixa preta: testar apenas métodos públicos


O que fazer Testar o interior levará a uma sobrecarga enorme e renderá quase nada. Se o seu código ou API fornecer os resultados corretos, vale a pena gastar três horas testando COMO funciona internamente e, então, oferecer suporte a esses testes frágeis? Quando você verifica o comportamento público, verifica simultaneamente implicitamente a própria implementação, seus testes falharão apenas se houver um problema específico (por exemplo, saída incorreta). Essa abordagem também é chamada de teste comportamental. Por outro lado, se você estiver testando os componentes internos (o método "caixa branca"), em vez de planejar a saída dos componentes, você se concentrará em pequenos detalhes e seus testes poderão ser interrompidos devido a pequenas alterações no código, mesmo se os resultados estiverem corretos, mas escolta terá muito mais recursos.

Caso contrário. Seus testes se comportarão como um garoto gritando "Lobo!" : Relate em voz alta os falsos positivos (por exemplo, o teste falha devido a uma alteração no nome de uma variável privada). Não é de surpreender que em breve as pessoas comecem a ignorar as notificações de IC e um dia perderão um bug real ...

Exemplos de código
Um exemplo de antipadrão. testando o interior sem uma boa razão.

Um exemplo usando o 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 Escolha a implementação simulada correta: evite objetos falsos em favor de stubs e espiões


O que fazer Implementações simuladas (duplas de teste) são um mal necessário, porque estão associadas às partes internas do aplicativo, e algumas são de grande valor ( atualize a memória de implementações imitadas: objetos falsos (zombarias), stubs (stubs) e objetos espiões (espiões) ) No entanto, nem todas as técnicas são equivalentes. Espiões e tocos são projetados para testar os requisitos, mas têm um efeito colateral inevitável - eles também afetam levemente o interior. E objetos falsos são projetados para testar o interior, o que leva a uma sobrecarga enorme, conforme descrito no capítulo 1.4.

Antes de usar implementações simuladas, faça a si mesmo a pergunta mais simples: “Eu uso isso para testar a funcionalidade que apareceu ou pode aparecer na documentação com requisitos?” Caso contrário, ele cheira a testes de caixa branca.

Por exemplo, se você quiser descobrir se o aplicativo se comporta como deveria quando o serviço de pagamento não está disponível, você pode fazer um esboço e retornar "Sem resposta" para verificar se o módulo em teste retorna o valor correto. Portanto, você pode verificar o comportamento / resposta / saída do aplicativo em certos cenários. Você também pode confirmar com a ajuda de um espião que, quando o serviço estava indisponível, a carta foi enviada, também é um teste comportamental, o que se reflete melhor na documentação com os requisitos ("Envie uma carta se as informações de pagamento não puderem ser salvas"). Ao mesmo tempo, se você criar um serviço de pagamento falso e garantir que ele seja chamado usando os tipos JS corretos, seu teste será direcionado para internos que não estão relacionados à funcionalidade do aplicativo e que provavelmente mudam com frequência.

Caso contrário. Qualquer refatoração de código envolve encontrar e atualizar todos os objetos falsos no código. Os testes de um amigo assistente se tornam um fardo.

Exemplos de código
Um exemplo de antipadrão. Objetos falsos são para tripas.

Exemplo usando 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(); }); 

Como fazer certo. Os espiões são projetados para testar os requisitos, mas há um efeito colateral - eles inevitavelmente afetam o interior.

 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 Não use "foo", use entrada realista


O que fazer Frequentemente, erros de produção ocorrem com dados de entrada muito específicos e surpreendentes. Quanto mais realistas os dados durante o teste, maior a probabilidade de detectar erros a tempo. Para gerar dados pseudo-reais que simulam a variedade e o tipo de dados de produção, use bibliotecas especiais, por exemplo, Faker . Essas bibliotecas podem gerar números de telefone realistas, apelidos de usuários, cartões bancários, nomes de empresas e até o texto "lorem ipsum". Você pode criar testes (além dos testes de unidade, e não em vez deles) que randomizam dados falsos para ajustar um módulo a um teste ou até importar dados reais de um ambiente de produção. Quer ir ainda mais longe? Leia o próximo capítulo (sobre testes baseados em propriedades).

Caso contrário. Seu teste de desenvolvimento terá êxito usando entradas sintéticas como "Foo", e os dados de produção poderão falhar quando um hacker @3e2ddsf . ##' 1 fdsfds . fds432 AAAA linha complicada como @3e2ddsf . ##' 1 fdsfds . fds432 AAAA @3e2ddsf . ##' 1 fdsfds . fds432 AAAA @3e2ddsf . ##' 1 fdsfds . fds432 AAAA .

Exemplos de código
Um exemplo de antipadrão. Um conjunto de testes que é executado com sucesso devido ao uso de dados irrealistas.

Um exemplo usando o 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 }); 

Como fazer certo. Aleatorize a entrada realista.

 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 Use testes baseados em propriedades para validar várias combinações de entradas


O que fazer Normalmente, para cada teste, selecionamos várias amostras de dados de entrada. Mesmo que o formato de entrada seja semelhante aos dados reais (consulte o capítulo “Não use“ foo ”), abordamos apenas algumas combinações de dados de entrada (método ('', true, 1) , método ("string" , false" , 0) Mas, em operação, uma API chamada com cinco parâmetros pode ser chamada com milhares de combinações diferentes, uma das quais pode levar a uma falha no processo ( distorção ) .E se você pudesse escrever um teste que envie automaticamente 1000 combinações de dados de entrada e fixação, em que combinações o código não retorna a resposta correta? A mesma coisa que fazemos com m todike teste com base nas propriedades: enviando todas as combinações possíveis de dados de entrada na unidade de teste que aumenta a chance de uma detecção de erros, por exemplo, temos um método. addNewProduct(id, name, isDiscount) Apoiar sua biblioteca irá chamar esse método com um número de combinações. (, , ) , por exemplo, (1, "iPhone", false) , (2, "Galaxy", true) etc.) Você pode testar com base nas propriedades usando seu executor de teste favorito (Mocha, Jest etc.) e bibliotecas como js-verifica ou testcheck (possui uma documentação muito melhor). Você também pode experimentar a biblioteca de verificação rápida , que oferece recursos adicionais e é acompanhada ativamente pelo autor.

Caso contrário. Você está escolhendo, sem pensar, dados de entrada para o teste, que abrange apenas caminhos de execução de código que funcionam bem. Infelizmente, isso reduz a eficácia dos testes como forma de detectar erros.

Exemplos de código
Como fazer certo. Teste várias combinações com o 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 Se necessário, use apenas fotos curtas e em linha.


O que fazer Quando você precisar testar com base em snapshots , use apenas snapshots curtos sem todo o extra (por exemplo, em 3 a 7 linhas), incluindo-os como parte do teste ( snapshot em linha ) e não como arquivos externos. Seguir esta recomendação manterá seus testes evidentes e mais confiáveis.

Por outro lado, os guias e ferramentas do “instantâneo clássico” nos levam a armazenar arquivos grandes (por exemplo, marcação para componentes de renderização ou resultados da API JSON) em mídia externa e comparar os resultados com a versão salva sempre que o teste é executado. Pode, digamos, associar implicitamente nosso teste a 1000 linhas contendo 3000 valores que o autor do teste nunca viu sobre o que ele não esperava. Por que isso é ruim? Porque existem 1000 razões para o teste falhar. Mesmo uma linha pode invalidar um instantâneo, e isso pode acontecer com frequência. Quanto Após cada espaço, comentário ou pequena alteração no CSS ou HTML. Além disso, o nome do teste não informa sobre a falha, porque verifica apenas se 1000 linhas não foram alteradas e também incentiva o autor do teste a levar, pelo tempo que desejar, um documento longo que ele não pôde analisar e verificar. Todos esses são sintomas de um teste obscuro e apressado, que não possui uma tarefa clara e está tentando realizar demais.

É importante notar que existem várias situações em que é aceitável usar imagens longas e externas, por exemplo, ao confirmar o esquema, e não os dados (extração de valores e foco nos campos) ou quando os documentos recebidos raramente mudam.

Caso contrário. Os testes de interface do usuário falham. O código parece bom, os pixels ideais são exibidos na tela, então o que acontece? Seu teste com instantâneos acabou de revelar a diferença entre o documento original e o recém-recebido - um caractere de espaço foi adicionado à marcação ...

Exemplos de código
Um exemplo de antipadrão. Associando um teste a algumas 2000 linhas de código desconhecidas.

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

Como fazer certo. As expectativas são visíveis e estão no centro das atenções.

 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 Evite bancos de teste globais e dados iniciais, adicione dados a cada teste separadamente


O que fazer De acordo com a regra de ouro (capítulo 0), cada teste deve adicionar e trabalhar dentro de seu próprio conjunto de linhas no banco de dados para evitar vinculações, e era mais fácil para os usuários entender o teste. Na realidade, os testadores frequentemente violam essa regra, antes de executar testes preenchendo o banco de dados com dados iniciais (sementes) ( também chamados de "banco de testes" ) para aumentar a produtividade. , (. « »), . . , , (, ).

. , , , ? , , , .

Um exemplo de antipadrão. Os testes não são independentes e usam algum tipo de gancho global para obter dados globais do banco de dados.

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

Como fazer certo. Você pode permanecer dentro do teste, cada teste funciona apenas com seus próprios dados.

 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-) , .

Um exemplo de antipadrão. Um longo caso de teste que tenta detectar um erro usando o 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'



Um exemplo:



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 %, . Como assim? , , , . . - : , , , .

, . 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(…) .

. , , . O que está havendo? , , . . , , .

. , . , ESLint production-.



5.2


. CI , , ..? , . Porque : (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/pt466879/


All Articles