Testes - Pensando de forma atômica

Atomic

Na aula Aula 12 parte 2 do nosso curso Be MEAN eu mostrei como criar um módulo automatizado para testar nossos Quarks de validação, pois os mesmos são bem simples, só devem retornar TRUE ou FALSE.

Nesse exemplo utilizo o Chai expect para validar o retorno dos Quarks e o Mocha para rodar os testes.

Primeiramente vamos ver como fazer um teste normal:

'use strict';

const expect = require('chai').expect;  
const valueTRUE = 'Suissa';  
const valueFALSE = 1;

describe('isString', () => {  
  describe('é String',  () => {
    it('testando: "'+valueTRUE+'"', () => {
      expect(require('./isString')(valueTRUE)).to.equal(true);
    });
  });
  describe('não é String',  () => {
    it('testando: "'+valueFALSE+'"', () => {
      expect(require('./isString')(valueFALSE)).to.equal(false);
    });
  });
});

Para rodar basta executar: mocha isString/isString.test.js

  isString
    é String
      ✓ testando: Suissa
    não é String
      ✓ testando: 1


  2 passing (28ms)

Pronto ele não teve erros pois validamos nossos testes corretamente, porém testamos apenas com 1 valor e isso é ridículo né?

Então vamos agora criar um teste que valide vários valores, para fazer isso iniciamos colocando os valores verdadeiros e falsos em arrays:

'use strict';

const expect = require('chai').expect;

const valuesTRUE = ['Suissa', '1', '', ' '];  
const valuesFALSE = [null, undefined, 1, true, {}, ()=>{}];

Certo? Então definimos em valuesTRUE todos os valores possíveis que devem ser aceitos como String e em valuesFALSE todos os valores que não podem ser String.

Agora criamos a estrutura para os 2 testes:

'use strict';

const expect = require('chai').expect;

const valuesTRUE = ['Suissa', '1', '', ' '];  
const valuesFALSE = [null, undefined, 1, true, {}, ()=>{}];

describe('isString', () => {  
  describe('é String',  () => {
  });
  describe('não é String',  () => {
  });
});

Bem simples não? Então basta fazer o que?

Criar uma função que itere sobre os valores do array e vá executando o it que é a função que irá testar realmente os valores:

it('testando: '+valueTRUE, () => {  
  expect(require('./isString')(valueTRUE)).to.equal(true);
});

Bom então sabemos que precisamos fazer 1 it para cada valor do array e obviamente não faremos isso manualmente, correto?

Então como faremos?

forEach

Mas como?

Dessa forma:

valuesTRUE.forEach( function(element, index) {  
  it('testando: '+element,  () => {
    expect(require('./isString')(element)).to.equal(true);
  });
});

Percebeu que ele irá criar 1 it dinamicamente para cada item do array valuesTRUE?

Agora basta juntarmos tudo para ficar assim:

'use strict';

const expect = require('chai').expect;

const valuesTRUE = ['Suissa', '1', '', ' '];  
const valuesFALSE = [null, undefined, 1, true, {}, ()=>{}];

describe('isString', () => {  
  describe('é String',  () => {
    valuesTRUE.forEach( function(element, index) {
      it('testando: '+element,  () => {
        expect(require('./isString')(element)).to.equal(true);
      });
    });
  });
  describe('não é String',  () => {
    valuesFALSE.forEach( (element, index) => {
      it('testando: '+element,  () => {
        expect(require('./isString')(element)).to.equal(false);
      });
    });
  });
});

Executando nosso teste ficará assim:

mocha isString/isString.test.js


  isString
    é String
      ✓ testando: Suissa
      ✓ testando: 1
      ✓ testando: 
      ✓ testando:  
    não é String
      ✓ testando: null
      ✓ testando: undefined
      ✓ testando: 1
      ✓ testando: true
      ✓ testando: [object Object]
      ✓ testando: ()=>{}


  10 passing (28ms)

Mas é óbvio que ainda podemos melhorar esse código refatorando-o, acompanhe comigo pois iremos separar as funções de teste dos describes:

'use strict';

const expect = require('chai').expect;

const valuesTRUE = ['Suissa', '1', '', ' '];  
const valuesFALSE = [null, undefined, 1, true, {}, ()=>{}];

const testTRUE = (values) => {  
  values.forEach( (element) => {
    it('testando: '+element,  () => {
      expect(require('./isString')(element)).to.equal(true);
    });
  });
};

const testFALSE = (values) => {  
  values.forEach( (element) => {
    it('testando: '+element,  () => {
      expect(require('./isString')(element)).to.equal(false);
    });
  });
};

describe('isString', () => {  
  describe('é String',  () => testTRUE(valuesTRUE));
  describe('não é String',  () => testFALSE(valuesFALSE));
});

OK! Mas para que isso?

Ahhhhhhhh! Você ainda não notou o padrão?

Então perceba essas duas funções:

  • testTRUE
  • testFALSE

Conseguiu ver o padrão agora?

Ainda não? Então vamos analisar!  
const testTRUE = (values) => {  
  values.forEach( (element) => {
    it('testando: '+element,  () => {
      expect(require('./isString')(element)).to.equal(true);
    });
  });
};

const testFALSE = (values) => {  
  values.forEach( (element) => {
    it('testando: '+element,  () => {
      expect(require('./isString')(element)).to.equal(false);
    });
  });
};
Vamos retirar apenas o miolo delas:

values.forEach( (element) => {  
  it('testando: '+element,  () => {
    expect(require('./isString')(element)).to.equal(true);
  });
});

values.forEach( (element) => {  
  it('testando: '+element,  () => {
    expect(require('./isString')(element)).to.equal(false);
  });
});

Agora sim você deve ter percebido que o único valor que mudou nas 2 foi... ???

O valor que passamos para função to.equal!
Pronto! Agora basta mudarmos esse valor para uma variável que as 2 funções ficarão iguais:

values.forEach( (element) => {  
  it('testando: '+element,  () => {
    expect(require('./isString')(element)).to.equal(valueToTest);
  });
});

values.forEach( (element) => {  
  it('testando: '+element,  () => {
    expect(require('./isString')(element)).to.equal(valueToTest);
  });
});

Aí você deve se perguntar:

De onde vem o valor de valueToTest?
Ótima pergunta! Vem pela função genérica que iremos criar:

'use strict';

const expect = require('chai').expect;

const valuesTRUE = ['Suissa', '1', '', ' '];  
const valuesFALSE = [null, undefined, 1, true, {}, ()=>{}];

const test = (values, valueToTest) => {  
  values.forEach( (element) => {
    it('testando: '+element,  () => {
      expect(require('./isString')(element)).to.equal(valueToTest);
    });
  });
};

describe('isString', () => {  
  describe('é String',  () => test(valuesTRUE, true));
  describe('não é String',  () => test(valuesFALSE, false));
});

Acho que agora temos um padrão bem simples e claro para utilizar em nossos testes, não?

Bom eu ainda quero refatorar mais um pouco, mas o que podemos fazer então?

Podemos ver que dentro do describe pai describe('isString' nós SEMPRE teremos apenas 2 describes:

describe('isString', () => {  
  describe('é String',  () => test(valuesTRUE, true));
  describe('não é String',  () => test(valuesFALSE, false));
});

Agora vamos analisar o padrão deles:

const messageTRUE = 'é String';  
const messageFALSE = 'não é String';

describe('isString', () => {  
  describe(messageTRUE,  () => test(valuesTRUE, true));
  describe(messageFALSE,  () => test(valuesFALSE, false));
});

Sabemos então que o describe é formado de:

  • mensagem para o teste
  • função que executa o teste

O que precisamos fazer é criar um objeto que possa agregar toda essa lógica, por exemplo:

const valuesTRUE = ['Suissa', '1', '', ' '];  
const valuesFALSE = [null, undefined, 1, true, {}, ()=>{}];  
const test = (values, valueToTest) => {  
  values.forEach( (element) => {
    it('testando: '+element,  () => {
      expect(require('./isString')(element)).to.equal(valueToTest);
    });
  });
};
const describes = [  
  {type: true, message: 'é String', test: test}
, {type: false, message: 'não é String', test: test}
];

Então em 1 objeto nós temos:

  • type: tipo do teste
  • message: mensagem do teste
  • test: função de validação

Estamos usando a mesma função genérica test criada anteriormente e agora como faço isso funcionar com o describe?

Então aqui precisamos entender que não podemos fazer como no it, que deixamos bem genérico, em vez disso precisamos obrigatoriamente ter 2 describes separados.

Podemos fazer da seguinte forma:

  1. itere no array describes
  2. teste o type do describe
  3. crie o describe correto a partir do type
  4. chame a função test corretamente

Fazendo isso nosso código ficará assim:

describe('isString', () => {  
  describes.forEach( (element, index) => {
    if(element.type) {
      describe(element.message,  () => {
        test(valuesTRUE, element.type);
      });
    }
    else {
      describe(element.message,  () => {
        test(valuesFALSE, element.type);
      });
    }
  });
});

Perceba que quando ele entrar em if(element.type) só entrará com o objeto com o type=true, nesse caso irá criar o describe correto para os teste que devem dar true e logo após no else cria o describe para os valores que devem dar false.

Juntando tudo isso nosso código ficou assim:

'use strict';

const expect = require('chai').expect;

const valuesTRUE = ['Suissa', '1', '', ' '];  
const valuesFALSE = [null, undefined, 1, true, {}, ()=>{}];  
const test = (values, valueToTest) => {  
  values.forEach( (element) => {
    it('testando: '+element,  () => {
      expect(require('./isString')(element)).to.equal(valueToTest);
    });
  });
};
const describes = [  
  {type: true, message: 'é String', test: test}
, {type: false, message: 'não é String', test: test}
]

describe('isString', () => {  
  describes.forEach( (element, index) => {
    if(element.type) {
      describe(element.message,  () => {
        test(valuesTRUE, element.type);
      });
    }
    else {
      describe(element.message,  () => {
        test(valuesFALSE, element.type);
      });
    }
  });
});

Agora execute ele no terminal:

mocha isString/isString.test.module.js


  isString
    é String
      ✓ testando: Suissa
      ✓ testando: 1
      ✓ testando: 
      ✓ testando:  
    não é String
      ✓ testando: null
      ✓ testando: undefined
      ✓ testando: 1
      ✓ testando: true
      ✓ testando: [object Object]
      ✓ testando: ()=>{}


  10 passing (16ms)

Agora sabe o que seria bom?

Dar uma refatoradinha marota!

MAS POR QUE CARAIOOOOO!!!??

Apenas observe:

'use strict';

const expect = require('chai').expect;

const test = (values, valueToTest) => {  
  values.forEach( (element) => {
    it('testando: '+element,  () => {
      expect(require('./isString')(element)).to.equal(valueToTest);
    });
  });
};
const describes = [  
  { type: true
  , message: 'é String'
  , test: test
  , values: ['Suissa', '1', '', ' ']
  }
, 
  { type: false
  , message: 'não é String'
  , test: test
  , values: [null, undefined, 1, true, {}, ()=>{}]
  }
]

});

Ah legal você só mudou os valores para dentro do objeto e aí?

Tente advinhar o porquê eu fiz isso!

Tá vou te ajudar.

Imagine que você quer transformar agora esse código em um módulo de testes genérico, como você faria? Que possa ser usado dessa forma:

const describes = [  
  { type: true
  , message: 'é String'
  , values: ['Suissa', '1', '', ' ']
  }
, 
  { type: false
  , message: 'não é String'
  , values: [null, undefined, 1, true, {}, ()=>{}]
  }
];
require('./index')('isString', describes);  

Vou lhe falar como eu faria então.

Perceba que não tenho mais a função test nesse objeto pois não é da responsabilidade dele conhecer essa função, sua única responsabilidade é ter os dados necessários para testar, nossa função test já é genérica para funcionar sem precisar ser definida anteriormente.

Beleza então vamos criar o testModule/testModule.js:

'use strict';

const expect = require('chai').expect;

module.exports = (testName, describes) => {  
}

Agora você entenderá o porquê passamos o testName também, antes de refatorarmos vamos analisar a função test:

const test = (values, valueToTest) => {  
  values.forEach( (element) => {
    it('testando: '+element,  () => {
      expect(require('./isString')(element)).to.equal(valueToTest);
    });
  });
};

Percebeu que o únco valor definido diretamente ali é o './isString'?

É exatamente por isso que passamos esse valor para nosso módulo em vez de definir manualmente, para que dessa forma ele possa funcionar com qualquer outro módulo.

Então nossa função refatorada fica assim:

const test = (values, valueToTest) => {  
  values.forEach( (element) => {
    it('testando: '+element,  () => {
      expect(require('./../'+testName+'/'+testName)(element)).to.equal(valueToTest);
    });
  });
};

Claro que precisamos definir um padrão de pastas para que funcione sem problemas, mas isso é assunto para outra aula :p

Depois disso basta colocar o testName no primeiro describe e pronto!

'use strict';

const expect = require('chai').expect;

module.exports = (testName, describes) => {

  const test = (values, valueToTest) => {
    values.forEach( (element) => {
      it('testando: '+element,  () => {
        expect(require('./../'+testName+'/'+testName)(element)).to.equal(valueToTest);
      });
    });
  };
  describe(testName, () => {
    describes.forEach( (element, index) => {
      describe(element.message,  () => {
        test(element.values, element.type);
      });
    });
  });

};

Agora sim você pode testar qualquer Quark nosso facilmente dessa forma:

'use strict';

const describes = [  
  { type: true
  , message: 'é String'
  , values: ['Suissa', '1', '', ' ']
  }
, 
  { type: false
  , message: 'não é String'
  , values: [null, undefined, 1, true, {}, ()=>{}]
  }
];
require('./testModule')('isString', describes);  

BEM MELHOR AGORA NÃO??!!

Minha ideia com isso é automatizar esses testes chatos para q vcs não precisem fazê-los manualmente, nesse caso nós testamos nossos Quarks que são basicamente funções de validação que retornam APENAS TRUE ou FALSE.

Espero que vocês tenham achado interessante, pois eu achei. :p

Caso você queira saber mais sobre os Quarks click aqui https://github.com/Webschool-io/Node-Atomic-Design_QUARKS

Até a próxima! :*

Comentários

comments powered by Disqus