javascript - ¿Cómo escribir una prueba unitaria para una clase tipada?
He estado aprendiendo a escribir pruebas unitarias de código JavaScript/TypeScript usando la biblioteca Jest. Aquí hay un ejemplo que no sé cómo abordar. Está tipado con TypeScript: solo hay dos métodos públicos y un constructor que requiere el argumento service1.
Creo que necesito probar dos situaciones:
si
this.attr es <= 42y ocurre la incremetación,si
this.attr es > 42y el métodoend()se dispara.
Mis problemas son:
No puedo acceder a la propiedad
attr, es privada y no sé cómo asignarle algún valor (quizás al crear una instancia en una prueba, pero no sé cómo)No sé qué es la función
this.service1.get(). No veo ninguna implementación de ella en el código y no sé cómo funciona. ¿Debería pasarla como argumento a una instancia de esta clase?Me confundí, ¿debería usar fakeTimers o mock/spy en este ejemplo en particular?
export class Class4 {
private attr: number;
private intervalId;
constructor(private service1) { }
public method() {
this.intervalId = setInterval(() => {
if (this.service1.get() > 42) {
this.end()
} else {
this.attr++;
}
}, 100);
}
public getAttr() {
return this.attr;
}
private end() {
clearInterval(this.intervalId);
}
}
Necesito tu ayuda para escribir un test en Jest solo para las 2 situaciones que describí.
Editar. Aquí hay una prueba simple basada en esta clase. No está asignando un valor a this.attr (el valor de mi argumento se asigna a service1 sin embargo) y después de ejecutar una prueba recibo un mensaje de error
Esperado: 40 Recibido: indefinido
Código :
it('should stop incrementing Class4.attr if it\'s > 42', () => {
const class4 = new Class4(40);
const attrVal = class4.getAttr();
expect(attrVal).toBe(40);
});
Respuesta #1
No estoy muy seguro de que esto pueda ayudar, pero a continuación hay un ejemplo de cómo podrías usar Jest para probar algo así.
Es tu código traducido de TypeScript a ES6 con una ligera implementación falsa de Jest adjunta. Está en un script separado para dejar el ejemplo en sí intacto.
El Jest falso solo implementa los matchers de Jest requeridos en esta prueba: expect, toBeGreaterThan, not, toHaveBeenCalledTimes.
Y las siguientes utilidades de Jest: useFakeTimers, advanceTimersByTime, clearAllTimers, mock
// self calling function is required to simulate Class4 module and for fake Jest mock to work
(function() {
// translated from typescript to es6
class Class4 {
attr = 0;
intervalId = null;
constructor(service1) {
this.service1 = service1;
}
method() {
this.intervalId = setInterval(() => {
if (this.service1.get() > 42) {
this.end();
} else {
this.attr++;
}
}, 100);
}
getAttr() {
return this.attr;
}
end() {
clearInterval(this.intervalId);
}
}
// this is required to simulate Class4 module and for fake Jest mock to work
window.Class4 = Class4;
})();
// even if we do not know exactly what Service is,
// we know that it has a get method which returns a varying number.
// so this implementation of Service will do
// (it's ok since we're testing Class4, not Service)
class ServiceImpl {
v = 0;
set(v) { this.v = v; }
get() { return this.v; }
}
// after this call, jest will control the flow of
// time in the following tests
// (reimplements the global methods setInterval, setTimeout...etc)
jest.useFakeTimers();
// actually it should be jest.mock('<path to your module>')
// but remember we're using a fake Jest working in SO's snippet)
// now Class4 is a mock
jest.mock(Class4);
// we need a Service instance for a Class4 object to be instanciated
const service = new ServiceImpl();
const class4 = new Class4(service);
it('Class4 constructor has been called 1 time', () => {
expect(Class4).toHaveBeenCalledTimes(1);
});
it('should be incrementing Class4.attr if service.get() < 42', () => {
// service.get() will return 40
service.set(40);
// storing initial attr val
let lastAttrVal = class4.getAttr();
// now class4 is running and should be incrementing
class4.method();
// jest controls the time, advances time by 1 second
jest.advanceTimersByTime(1000);
expect(class4.getAttr()).toBeGreaterThan(lastAttrVal);
});
it('should have been called Class4.end 0 time', () => {
expect(Class4.mock.instances[0].end).toHaveBeenCalledTimes(0);
});
it('should stop incrementing Class4.attr if service.get() > 42', () => {
// service.get() will now return 45, this should end class4
// incrementation in the next interval
service.set(45);
// storing current attr val
let lastAttrVal = class4.getAttr();
jest.advanceTimersByTime(1000);
expect(class4.getAttr()).not.toBeGreaterThan(lastAttrVal);
});
it('end should have been called end 1 time', () => {
expect(Class4.mock.instances[0].end).toHaveBeenCalledTimes(1);
});
jest.clearAllTimers();<script type="text/javascript">
window.jest = {};
jest.useFakeTimers = () => {
jest.oldSetTimeout = window.setTimeout;
jest.oldSetInterval = window.setInterval;
jest.oldClearTimeout = window.clearTimeout;
jest.oldClearInterval = window.clearInterval;
jest.time = 0;
jest.runningIntervals = [];
window.setInterval = (callback, delay) => {
let interIndex = jest.runningIntervals.findIndex(i => i.cancelled);
let inter = interIndex !== -1 && jest.runningIntervals[interIndex];
if (!inter) {
inter = {};
interIndex = jest.runningIntervals.length;
jest.runningIntervals.push(inter);
}
Object.assign(
inter,
{
start: jest.time,
last: jest.time,
callback,
delay,
cancelled: false
}
);
callback();
return interIndex;
};
window.clearInterval = idx => {
jest.runningIntervals[idx].cancelled = true;
};
jest.advanceTimersByTime = advance => {
for (const end = jest.time + advance;jest.time < end; jest.time++) {
jest.runningIntervals.forEach(inter => {
if (!inter.cancelled && jest.time - inter.last >= inter.delay) {
inter.last = jest.time;
inter.callback();
}
});
}
};
jest.clearAllTimers = () => {
jest.runningIntervals.length = 0;
window.setTimeout = jest.oldSetTimeout;
window.setInterval = jest.oldSetInterval;
window.clearTimeout = jest.oldClearTimeout;
window.clearInterval = jest.oldClearInterval;
};
};
jest.resolve = (v) => {
console.log(v ? 'PASS' : 'FAIL');
}
window.it = (description, test) => {
console.log(description);
test();
};
window.expect = (received) => {
return {
toBeGreaterThan: (expected) => jest.resolve(received > expected),
not: {
toBeGreaterThan: (expected) => jest.resolve(received <= expected),
},
toHaveBeenCalledTimes: (expected) => jest.resolve((received ? received.mock.calls.length : 0) === expected),
}
}
jest.mock = (cls) => {
if (cls.mock) return;
const mock = {
instances: [],
calls: []
}
const proto0 = cls.prototype;
function ClassMock(...args) {
mock.calls.push(args);
this.instance = new proto0.constructor(...args);
this.instanceMock = {};
mock.instances.push(this.instanceMock);
Object.getOwnPropertyNames(proto0).forEach((member) => {
if (member === 'constructor' || typeof proto0[member] !== 'function') return;
this.instanceMock[member] = this.instanceMock[member] || { mock: { calls: [] } };
this.instance[member] = (function(...args) {
this.instanceMock[member].mock.calls.push(args);
return proto0[member].apply(this.instance, [args]);
}).bind(this);
});
}
Object.getOwnPropertyNames(proto0).forEach((member) => {
if (member === 'constructor' || typeof proto0[member] !== 'function') return;
ClassMock.prototype[member] = function(...args) {
return this.instance[member](...args);
}
});
ClassMock.mock = mock;
window[proto0.constructor.name] = ClassMock;
}
</script>