Las mejores técnicas de prueba en JavaScript y Node.js


Esta es una guía completa para proporcionar confiabilidad en JavaScript y Node.js. Aquí se recopilan docenas de las mejores publicaciones, libros y herramientas.

Primero, trate con los métodos de prueba generalmente aceptados que subyacen a cualquier aplicación. Y luego puede profundizar en el área de interés para usted: interfaz e interfaces, backend, CI o todo lo anterior.

Contenido



Sección 0. Regla de oro


0. Regla de oro: apegarse a las pruebas Lean


Que hacer El código de prueba es diferente de lo que entra en funcionamiento. Hazlo lo más simple posible, corto, libre de abstracciones, soltero, maravilloso en el trabajo y económico. Otra persona debe mirar la prueba e inmediatamente comprender lo que está haciendo.

Nuestros jefes están ocupados con el código de producción, no tienen espacio libre para una complejidad adicional. Si introducimos una nueva porción de código complejo en nuestra pobre mente, esto ralentizará el trabajo de todo el equipo en la tarea, por lo que estamos probando. De hecho, debido a esto, muchos equipos simplemente evitan las pruebas.

Pruebas: esta es una oportunidad para obtener un asistente amable y sonriente, con quien es muy bueno trabajar y que ofrece un gran retorno de las pequeñas inversiones. Los científicos creen que en nuestro cerebro hay dos sistemas: uno para acciones que no requieren esfuerzo, como conducir en una carretera vacía, y el segundo para operaciones complejas que requieren conciencia, como resolver ecuaciones matemáticas. Cree sus pruebas para el primer sistema, de modo que cuando mire el código tenga una sensación de simplicidad comparable a la edición de un documento HTML, y no con una solución 2X(17 × 24) .

Esto se puede lograr mediante una cuidadosa selección de métodos, herramientas y objetivos para las pruebas, de modo que sean económicos y den un gran retorno de la inversión. Pruebe solo tanto como sea necesario, trate de ser flexible. A veces vale la pena incluso descartar algunas pruebas y sacrificar la fiabilidad en aras de la velocidad y la simplicidad.



La mayoría de las recomendaciones a continuación se derivan de este principio.
Estas listo

Sección 1. Anatomía de la prueba.


1.1 El nombre de cada prueba debe constar de tres partes


Que hacer El informe de prueba debe indicar si la revisión actual de la aplicación cumple con los requisitos de aquellas personas que no están familiarizadas con el código: probadores involucrados en el despliegue de ingenieros de DevOps, así como usted mismo en dos años. Será mejor si las pruebas informan la información en el idioma de los requisitos, y sus nombres constan de tres partes:

  1. ¿Qué se está probando exactamente? Por ejemplo, el método ProductsService.addNewProduct .
  2. ¿Bajo qué condiciones y escenarios? Por ejemplo, el precio no se pasa al método.
  3. ¿Cuál es el resultado esperado? Por ejemplo, un nuevo producto no está aprobado.

De lo contrario. La implementación falla, la prueba llamada "Agregar producto" falla. ¿Entiendes qué funciona exactamente mal?

Nota Cada capítulo tiene un código de ejemplo y, a veces, una ilustración. Ver spoilers.

Ejemplos de código
Cómo hacerlo bien El nombre de la prueba consta de tres 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 Estructurar las pruebas de acuerdo con el patrón AAA


Que hacer Cada prueba debe constar de tres secciones claramente separadas: Organizar (preparación), Actuar (acción) y Afirmar (resultado). Adherirse a dicha estructura asegura que el lector de su código no tenga que usar el procesador cerebral para comprender el plan de prueba:

Organizar: todo el código que lleva al sistema al estado según el escenario de prueba. Esto puede incluir crear una instancia del módulo en el diseñador de prueba, agregar registros a la base de datos, crear apéndices en lugar de objetos y cualquier otro código que prepare el sistema para la ejecución de la prueba.

Ley: ejecución de código como parte de una prueba. Por lo general, solo una línea.
Afirmar: asegúrese de que el valor obtenido cumpla con las expectativas. Por lo general, solo una línea.

De lo contrario. No solo pasará largas horas trabajando con el código principal, sino que su cerebro también se hinchará por lo que debería ser un trabajo simple: por las pruebas.

Ejemplos de código
Cómo hacerlo bien Una prueba estructurada de acuerdo con el patrón 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'); }); }); 

Un ejemplo de antipatrón. Ninguna separación, en una sola pieza, es más 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 Describa las expectativas en el lenguaje del producto: estado al estilo de BDD


Que hacer La programación de pruebas en un estilo declarativo permite al usuario comprender de inmediato la esencia sin gastar un solo ciclo de procesador cerebral. Cuando escribe código imperativo empaquetado en lógica condicional, el lector tiene que hacer un gran esfuerzo. Desde este punto de vista, debe describir las expectativas en un lenguaje similar al humano en un estilo de BDD declarativo utilizando expect / should y no utilizando un código personalizado. Si en Chai y Jest no hay una afirmación necesaria, que a menudo se repite, entonces puede expandir el emparejador Jest o escribir su propio complemento para Chai .

De lo contrario. El equipo escribirá menos pruebas y decorará pruebas molestas with .skip() .

Ejemplos de código
Un ejemplo usando Mocha .

Un ejemplo de antipatrón. Para comprender la esencia de la prueba, el usuario se ve obligado a pasar por un código imperativo bastante largo.

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

Cómo hacerlo bien Leer esta prueba declarativa es sencillo.

 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 Adhiérase a las pruebas de recuadro negro: pruebe solo los métodos públicos


Que hacer Probar el interior conducirá a una gran sobrecarga y no producirá casi nada. Si su código o API proporciona los resultados correctos, ¿vale la pena pasar tres horas probando CÓMO funciona internamente y luego también admitir estas pruebas frágiles? Cuando verifica el comportamiento público, simultáneamente verifica implícitamente la implementación en sí, sus pruebas fallarán solo si hay un problema específico (por ejemplo, salida incorrecta). Este enfoque también se llama prueba de comportamiento. Por otro lado, si está probando las partes internas (el método del "recuadro blanco"), en lugar de planificar la salida de los componentes, se centrará en pequeños detalles, y sus pruebas pueden romperse debido a pequeñas alteraciones del código, incluso si los resultados son correctos, pero La escolta tomará muchos más recursos.

De lo contrario. Sus pruebas se comportarán como un niño gritando "¡Lobo!" : Informar en voz alta falsos positivos (por ejemplo, la prueba falla debido a un cambio en el nombre de una variable privada). No es sorprendente que pronto las personas comiencen a ignorar las notificaciones de CI, y algún día se perderán un error real ...

Ejemplos de código
Un ejemplo de antipatrón. probando el interior sin una buena razón.

Un ejemplo usando 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 Elija la implementación simulada correcta: evite objetos falsos en favor de trozos y espías


Que hacer Las implementaciones simuladas (dobles de prueba) son un mal necesario porque están asociadas con las partes internas de la aplicación, y algunas son de gran valor ( refresca la memoria de implementaciones imitadas: objetos falsos (simulacros), stubs (stubs) y objetos espías (espías) ) Sin embargo, no todas las técnicas son equivalentes. Los espías y los trozos están diseñados para probar los requisitos, pero tienen un efecto secundario inevitable: también afectan ligeramente el interior. Y los objetos falsos están diseñados para probar el interior, lo que conduce a una gran sobrecarga, como se describe en el capítulo 1.4.

Antes de usar implementaciones simuladas, hágase la pregunta más simple: "¿Utilizo esto para probar la funcionalidad que ha aparecido o puede aparecer en la documentación con requisitos?" Si no, huele a pruebas de caja blanca.

Por ejemplo, si desea averiguar si la aplicación se comporta como debería cuando el servicio de pago no está disponible, puede hacer un talón en su lugar y devolver "Sin respuesta" para verificar si el módulo bajo prueba devuelve el valor correcto. Por lo tanto, puede verificar el comportamiento / respuesta / salida de la aplicación en ciertos escenarios. También puede confirmar con la ayuda de un espía que cuando el servicio no estaba disponible, se envió la carta, también es una prueba de comportamiento, que se refleja mejor en la documentación con los requisitos ("Enviar una carta si no se puede guardar la información de pago"). Al mismo tiempo, si realiza un servicio de pago falso y se asegura de que se llame utilizando los tipos JS correctos, su prueba está dirigida a elementos internos que no están relacionados con la funcionalidad de la aplicación y que es probable que cambien a menudo.

De lo contrario. Cualquier refactorización de código implica encontrar y actualizar todos los objetos falsos en el código. Las pruebas de un amigo asistente se convierten en una carga.

Ejemplos de código
Un ejemplo de antipatrón. Los objetos falsos son para las tripas.

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

Cómo hacerlo bien Los espías están diseñados para probar los requisitos, pero hay un efecto secundario: inevitablemente afectan el 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 No use "foo", use una entrada realista


Que hacer A menudo, los errores de producción se producen con datos de entrada muy específicos y sorprendentes. Cuanto más realistas sean los datos durante las pruebas, más probabilidades hay de detectar errores a tiempo. Para generar datos pseudo-reales que simulan la variedad y el tipo de datos de producción, use bibliotecas especiales, por ejemplo, Faker . Dichas bibliotecas pueden generar números de teléfono realistas, apodos de usuarios, tarjetas bancarias, nombres de compañías, incluso el texto "lorem ipsum". Puede crear pruebas (además de pruebas unitarias, y no en lugar de ellas) que aleatorizan datos falsos para ajustar el módulo a una prueba, o incluso importar datos reales desde un entorno de producción. ¿Quieres ir aún más lejos? Lea el siguiente capítulo (sobre pruebas basadas en propiedades).

De lo contrario. Sus pruebas de desarrollo se verán exitosas usando entradas sintéticas como "Foo", y los datos de producción pueden fallar cuando un hacker @3e2ddsf . ##' 1 fdsfds . fds432 AAAA línea difícil como @3e2ddsf . ##' 1 fdsfds . fds432 AAAA @3e2ddsf . ##' 1 fdsfds . fds432 AAAA @3e2ddsf . ##' 1 fdsfds . fds432 AAAA .

Ejemplos de código
Un ejemplo de antipatrón. Un conjunto de pruebas que se ejecuta con éxito debido al uso de datos poco realistas.

Un ejemplo usando 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 }); 

Cómo hacerlo bien Aleatorizar la 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 Usar pruebas basadas en propiedades para validar múltiples combinaciones de entrada


Que hacer Por lo general, para cada prueba seleccionamos varias muestras de datos de entrada. Incluso si el formato de entrada es similar a los datos reales (vea el capítulo "No use" foo "), cubrimos solo unas pocas combinaciones de datos de entrada (método ('', true, 1) , método ("string" , false" , 0) Pero en funcionamiento, una API que se llama con cinco parámetros se puede llamar con miles de combinaciones diferentes, una de las cuales puede provocar un bloqueo del proceso ( fuzzing ). ¿Qué pasaría si pudiera escribir una prueba que envíe automáticamente 1000 combinaciones de datos de entrada y ¿En qué combinaciones el código no devuelve la respuesta correcta? Lo mismo que hacemos con m todike prueba basada en las propiedades: mediante el envío de todas las combinaciones posibles de los datos de entrada en la unidad de prueba que aumentar la posibilidad de una detección de errores, por ejemplo, tenemos un método. addNewProduct(id, name, isDiscount) Apoyo a su biblioteca llamará a este método con una serie de combinaciones. (, , ) , por ejemplo, (1, "iPhone", false) , (2, "Galaxy", true) , etc. Puede realizar pruebas en función de las propiedades utilizando su corredor de prueba favorito (Mocha, Jest etc.) y bibliotecas como js- verified o testcheck (tiene una documentación mucho mejor). También puede probar la biblioteca de verificación rápida , que ofrece características adicionales y está activamente acompañada por el autor.

De lo contrario. Está eligiendo sin pensar los datos de entrada para la prueba, que cubre solo las rutas de ejecución de código que funcionan bien. Desafortunadamente, esto reduce la efectividad de las pruebas como medio para detectar errores.

Ejemplos de código
Cómo hacerlo bien Prueba numerosas combinaciones con el test de mocha-test.

 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 Si es necesario, use solo tomas cortas y en línea.


Que hacer Cuando necesite realizar una prueba basada en instantáneas , use solo instantáneas cortas sin todo el extra (por ejemplo, en 3-7 líneas), incluidas como parte de la prueba ( Instantánea en línea ), y no como archivos externos. Seguir esta recomendación mantendrá sus pruebas evidentes y más confiables.

Por otro lado, las guías y herramientas de "instantánea clásica" nos provocan que almacenemos archivos grandes (por ejemplo, marcado para renderizar componentes o resultados de API JSON) en medios externos y comparar los resultados con la versión guardada cada vez que se ejecuta la prueba. Puede, por ejemplo, asociar implícitamente nuestra prueba con 1000 líneas que contienen 3000 valores que el autor de la prueba nunca vio sobre lo que no había esperado. ¿Por qué es esto malo? Porque hay 1000 razones para que la prueba falle. Incluso una línea puede invalidar una instantánea, y esto puede suceder a menudo. Cuanto Después de cada espacio, comentario o cambio menor en CSS o HTML. Además, el nombre de la prueba no le informará sobre la falla, ya que solo verifica que 1000 líneas no hayan cambiado, y también alienta al autor de la prueba a tomar el tiempo que desee un documento largo que no pudo analizar y verificar. Todos estos son síntomas de una prueba oscura y apresurada que no tiene una tarea clara y está tratando de lograr demasiado.

Vale la pena señalar que hay varias situaciones en las que es aceptable usar imágenes largas y externas, por ejemplo, al confirmar el esquema, y ​​no los datos (extracción de valores y enfoque en los campos), o cuando los documentos recibidos rara vez cambian.

De lo contrario. Las pruebas de IU fallan. El código se ve bien, los píxeles ideales se muestran en la pantalla, entonces, ¿qué sucede? Su prueba con instantáneas acaba de revelar la diferencia entre el documento original y el recién recibido: se agregó un carácter de espacio al marcado ...

Ejemplos de código
Un ejemplo de antipatrón. Asociar una prueba con algunas 2000 líneas de código desconocidas.

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

Cómo hacerlo bien Las expectativas son visibles y están en el centro de atención.

 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 pruebas globales y datos iniciales, agregue datos a cada prueba por separado


Que hacer De acuerdo con la regla de oro (capítulo 0), cada prueba debería agregar y funcionar dentro de su propio conjunto de filas en la base de datos para evitar enlaces, y fue más fácil para los usuarios comprender la prueba. En realidad, los evaluadores a menudo violan esta regla, antes de ejecutar pruebas que llenan la base de datos con datos iniciales (semillas) ( también llamados "banco de pruebas" ) para aumentar la productividad. , (. « »), . . , , (, ).

. , , , ? , , , .

. - .

 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'



Un ejemplo:



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 %, . ¿Cómo es eso? , , , . . - : , , , .

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

. , , . Que esta pasando , , . . , , .

. , . , ESLint production-.



5.2


. CI , , ..? , . Por qué : (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/466879/


All Articles