Cuando ejecutamos los tests, siempre queremos que todos pasen en “verde”. Ese verde no siempre asegura que todo funcione a la perfección… siempre sale algún bug 🙁 . El motivo más común, es no hacer los suficientes o mejores tests como para cubrir todos los casos posibles. No obstante, en el artículo de hoy, la causa que pretendemos solucionar es que no se ejecute ningún expect() dentro del test (raro, ¿no?). Entonces, ¿para qué haces el test si luego no se comprueba si pasa o no pasa? En definitiva, la pregunta es ¿Cómo asegurar que se ejecuta un expect de jasmine en un test? (al menos como mínimo uno).

Al escribir el cuerpo de un test, básicamente seguimos el concepto de tripe A (“AAA”) 

  1. Arrange: Preparación del test (variables, mocks, …)
  2. Act: Ejecución del método (porción de código) que queremos testear
  3. Assert: Comprobación si lo que hace el método es lo deseado.

En jasmine, en el bloque “Assert” debemos asegurarnos que se ejecute como mínimo un “expect()”. 

Proyecto por defecto en angular cli

Si creamos un proyecto angular mediante angular cli con tests («ng new execute-expect») y ejecutamos los tests que crea por defecto («ng test») para el componente “app” («app.component.spec.ts»), en principio pasarán si no tocamos nada.

Código del test:

import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
 
describe('AppComponent', () => {
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
    }).compileComponents();
  });
 
  it('should create the app', () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.componentInstance;
    expect(app).toBeTruthy();
  });
 
  it(`should have as title 'execute-expect'`, () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.componentInstance;
    expect(app.title).toEqual('execute-expect');
  });
 
  it('should render title', () => {
    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    const compiled = fixture.nativeElement as HTMLElement;
    expect(compiled.querySelector('.content span')?.textContent).toContain('execute-expect app is running!');
  });
});

Al ejecutar “ng test”:

por consola:

Chrome 94.0.4606.81 (Windows 10): Executed 3 of 3 SUCCESS (0.259 secs / 0.242 secs)
TOTAL: 3 SUCCESS
Resultados en navegador de tests jasmine creados por defecto en angular cli
Resultados en navegador de tests jasmine creados por defecto en angular cli

Ahora bien, si eliminamos o comentamos un “expect()” de cualquiera de sus tests, el resultado es que pasan los tests, pero solo te da un aviso (warning).

Siendo por consola un mensaje “Spec ‘xxx’ has no expectations.”

WARN: 'Spec 'AppComponent should create the app' has no expectations.'
Chrome 94.0.4606.81 (Windows 10): Executed 2 of 3 SUCCESS (0 secs / 0.257 secs)
Chrome 94.0.4606.81 (Windows 10): Executed 3 of 3 SUCCESS (0.315 secs / 0.271 secs)
TOTAL: 3 SUCCESS

y en el navegador el mensaje “SPEC HAS NO EXPECTATIONS xxx”:

Resultados en navegador de tests jasmine creados por defecto en angular cli sin un expect
Resultados en navegador de tests jasmine creados por defecto en angular cli sin un expect

Estos mensajes de aviso («warnings»), viene porque en la configuración del “karma.conf.js”, tenemos que use el reporter ‘kjhtml’ que es quien envía este aviso.

reporters: ['progress', 'kjhtml']

Pero ¿y si resulta que no tenemos ese reporter configurado en el karma.conf.js? Pues bien, si no está ese (u otro que avise del warning) veríamos que todos los tests pasan sin ningún tipo de aviso. Y si está pero no hacemos caso de los warnings …. mejor hacérselo mirar.

Si estamos en un proyecto grande, lo lógico es que no se debería dejar pasar ese warning porque llevaríamos a producción un software sin testear partes del código. Entonces, lo ideal es no dejar pasar ese caso, es decir, en vez de un aviso, devuelva un error.

Devolver un error si no se ejecuta como mínimo un expect

Si tienes una versión de jasmine 3.5.0 ó superior hacerlo es muy fácil, dado que se puede configurar jasmine con una propiedad llamada “failSpecWithNoExpectations” en el karma.conf.js de la siguiente manera:

client: {
      jasmine: {
        failSpecWithNoExpectations: true
      },
      clearContext: false // leave Jasmine Spec Runner output visible in browser
    },

En el caso de tener una versión anterior a la 3.5.0 de jasmine no podrás aplicar esa configuración. Si es así sigue leyendo para darte una alternativa. Seguramente no sea la solución a todos los problemas o no guste, pero como mínimo es una primera idea a llevar código testeado.

Para llevar a cabo esta solución, echaremos mano de los custom reporters, es decir, crearemos un reporter propio, para que si al ejecutar un test no se ejecute como mínimo un expect, de un error y no un warning. 

No profundizaremos mucho, pero como se puede comprobar en su web, existe un ciclo de vida de la ejecución de los tests y nuestro reporter puede atender a cada estado de ese ciclo el cual llama a un método en concreto (jasmineStarted, suiteStarted, specStarted,specDone, suiteDone, jasmineDone) para hacer varias acciones. En el caso que nos ocupa, debemos atender cuando finalice un test hacer la comprobación si se ha ejecutado un expect o no, para ello debemos programar el método “specDone”.

En cada método, se recibe por parámetro cierta información necesaria para cada caso, en el nuestro, “specDone” recibe un objeto SpecResult. Si queremos comprobar si se ha ejecutado como mínimo un expect, entonces debemos ver si la propiedad “passedExpectations” no contiene nada (array vacío) y que el test esté correcto, entonces emitimos una excepción throw para que el test no pase, y escribir un mensaje acorde al error.

CustomReporter (p.ej: mandatory-expect-reporter.js):

var mandatoryExpectReporter = {
    specDone: (result) => {
        if (result.status === 'passed' && result.passedExpectations.length === 0) {
            const errorMessage = '\033[31m' + `Spec ${result.fullName} has no expectations.` + '\033[0m';
            throw errorMessage;
        }
    }
};

jasmine.getEnv().addReporter(mandatoryExpectReporter);

Una vez tenemos el código escrito del reporter, lo registramos en el mismo fichero usando el método addReporter de jasmine.

Para registrar este reporter, en el karma.conf.js, en el apartado files, añadimos una línea para leer este código:

files: [
        './src/mandatory-expect-reporter.js'
    ],

karma-parallel

Si usas karma-parallel al menos la versión 0.3.1 del que hicimos un artículo al respecto (puedes leerlo aquí) y tienes activado el failSpecWithNoExpectations, los extra tests que añade en ciertos casos (lee su documentación, apartado “important notes”) darán error porque estos tests que añade no tienen dentro ningún expect. Este bug ha sido reportado por nuestra parte.

En el caso de tener el custom reporter, podemos comprobar que si es un test creado de karma parallel, entonces que no se emita ninguna excepción. La condición sería ver si el nombre del test incluye el texto «[karma-parallel]»:

var mandatoryExpectReporter = {
    specDone: (result) => {
        if (result.status === 'passed' 
            && result.passedExpectations.length === 0 
            && result.fullName.indexOf('[karma-parallel]') === -1) {

            const errorMessage = '\033[31m' + `Spec ${result.fullName} has no expectations.` + '\033[0m';
            throw errorMessage;
        }
    }
};

jasmine.getEnv().addReporter(mandatoryExpectReporter);