Broma: cómo burlarse de un método específico de una clase

7 minutos de lectura

Broma: cómo burlarse de un método específico de una clase
CrazySynthax

Supongamos que tengo la siguiente clase:

export default class Person {
    constructor(first, last) {
        this.first = first;
        this.last = last;
    }
    sayMyName() {
        console.log(this.first + " " + this.last);
    }
    bla() {
        return "bla";
    }
}

Supongamos que quiero crear una clase simulada donde el método ‘sayMyName’ se simulará y el método ‘bla’ permanecerá como está.

La prueba que escribí es:

const Person = require("../Person");

jest.mock('../Person', () => {
    return jest.fn().mockImplementation(() => {
        return {sayMyName: () => {
            return 'Hello'
        }};
    });
});


let person = new Person();
test('MyTest', () => {
    expect(person.sayMyName()).toBe("Hello");
    expect(person.bla()).toBe("bla");
})

La primera declaración ‘esperar’ pasa, lo que significa que ‘sayMyName’ se burló con éxito. Pero, el segundo ‘esperar’ falla con el error:

TypeError: person.bla no es una función

Entiendo que la clase simulada borró todos los métodos. Quiero saber cómo simular una clase de modo que solo se simularán métodos específicos.

Utilizando jest.spyOn() es el adecuado Broma manera de burlarse de un solo método y dejar el resto en paz. En realidad, hay dos enfoques ligeramente diferentes para esto.

1. Modificar el método solo en un solo objeto

import Person from "./Person";

test('Modify only instance', () => {
    let person = new Person('Lorem', 'Ipsum');
    let spy = jest.spyOn(person, 'sayMyName').mockImplementation(() => 'Hello');

    expect(person.sayMyName()).toBe("Hello");
    expect(person.bla()).toBe("bla");

    // unnecessary in this case, putting it here just to illustrate how to "unmock" a method
    spy.mockRestore();
});

2. Modifique la clase en sí, para que todas las instancias se vean afectadas.

import Person from "./Person";

beforeAll(() => {
    jest.spyOn(Person.prototype, 'sayMyName').mockImplementation(() => 'Hello');
});

afterAll(() => {
    jest.restoreAllMocks();
});

test('Modify class', () => {
    let person = new Person('Lorem', 'Ipsum');
    expect(person.sayMyName()).toBe("Hello");
    expect(person.bla()).toBe("bla");
});

Y en aras de la exhaustividad, así es como te burlarías de un método estático:

jest.spyOn(Person, 'myStaticMethod').mockImplementation(() => 'blah');

  • Es una pena que la documentación oficial no diga esto. La documentación oficial es un lío incomprensible de fábricas de módulos, simulacros de clases hechos a mano usando literales de objetos, cosas almacenadas en directorios especiales y procesamiento de casos especiales basados ​​en nombres de propiedades. jestjs.io/docs/es/…

    – Neutrino

    24 ene.


  • He editado mi respuesta anterior. @blade usando mockImplementation siempre arrojará una prueba de aprobación y si la clase cambia, un falso positivo.

    – pollo al sésamo

    03 may. 21 en 16:56

  • @sesamechicken No estoy seguro de seguir con la prueba que siempre pasa. Nunca debe probar un método simulado directamente, eso no tendría ningún sentido. Un caso de uso habitual es simular un método que proporciona datos para un método diferente que en realidad está probando.

    – espada

    05 may. 21 en 12:07

  • > Nunca debe probar un método simulado directamente Eso es exactamente lo que demuestra el ejemplo anterior usando mockImplementation.

    – pollo al sésamo

    06 may. 21 en 13:54

  • Lo hace, claro, pero responde la pregunta del OP. Creo que estaría fuera de tema entrar en detalles de las pruebas correctas en esta respuesta y restaría valor al punto.

    – espada

    07 may. 21 en 16:04

Broma: cómo burlarse de un método específico de una clase
pollo al sésamo

Editar 03/05/2021

Veo que varias personas no están de acuerdo con el siguiente enfoque, y eso es genial. Sin embargo, tengo un ligero desacuerdo con el enfoque de @blade, ya que en realidad no prueba la clase porque está usando mockImplementation. Si la clase cambia, las pruebas siempre pasarán dando falsos positivos. Así que aquí hay un ejemplo con spyOn.

// person.js
export default class Person {
  constructor(first, last) {
      this.first = first;
      this.last = last;
  }
  sayMyName() {
      return this.first + " " + this.last; // Adjusted to return a value
  }
  bla() {
      return "bla";
  }
}

y la prueba:

import Person from './'

describe('Person class', () => {
  const person = new Person('Guy', 'Smiley')

  // Spying on the actual methods of the Person class
  jest.spyOn(person, 'sayMyName')
  jest.spyOn(person, 'bla')
  
  it('should return out the first and last name', () => {  
    expect(person.sayMyName()).toEqual('Guy Smiley') // deterministic 
    expect(person.sayMyName).toHaveBeenCalledTimes(1)
  });
  it('should return bla when blah is called', () => {
    expect(person.bla()).toEqual('bla')
    expect(person.bla).toHaveBeenCalledTimes(1)
  })
});

¡Salud! 🍻


No veo cómo la implementación simulada realmente resuelve algo para usted. Creo que esto tiene un poco más de sentido.

import Person from "./Person";

describe("Person", () => {
  it("should...", () => {
    const sayMyName = Person.prototype.sayMyName = jest.fn();
    const person = new Person('guy', 'smiley');
    const expected = {
      first: 'guy',
      last: 'smiley'
    }

    person.sayMyName();

    expect(sayMyName).toHaveBeenCalledTimes(1);
    expect(person).toEqual(expected);
  });
});

  • No sé la respuesta a esto, tan genuinamente curioso: ¿dejaría esto alterado Person.prototype.sayMyName para cualquier otra prueba que se ejecute después de esta?

    – Martín

    08 oct.

  • @Martin Sí, lo hace.

    – Frondor

    15 oct.

  • No creo que sea una buena práctica. No está usando Jest ni ningún otro marco para simular el método y necesitará un esfuerzo adicional para restaurar el método.

    –Bruno Brant

    04 sep.

  • Consulte stackoverflow.com/a/56565849/1248209 para obtener respuestas sobre cómo hacer esto correctamente en Jest usando spyOn.

    – Lockyy

    21 oct.

Broma: cómo burlarse de un método específico de una clase
madhamster

He estado haciendo una pregunta similar y creo que encontré una solución. Esto debería funcionar sin importar dónde se use realmente la instancia de la clase Person.

const Person = require("../Person");

jest.mock("../Person", function () {
    const { default: mockRealPerson } = jest.requireActual('../Person');

    mockRealPerson.prototype.sayMyName = function () {
        return "Hello";
    }    

    return mockRealPerson
});

test('MyTest', () => {
    const person = new Person();
    expect(person.sayMyName()).toBe("Hello");
    expect(person.bla()).toBe("bla");
});

en lugar de burlarse de la clase, podría extenderla así:

class MockedPerson extends Person {
  sayMyName () {
    return 'Hello'
  }
}
// and then
let person = new MockedPerson();

Si está utilizando Typescript, puede hacer lo siguiente:

Person.prototype.sayMyName = jest.fn().mockImplementationOnce(async () => 
        await 'my name is dev'
);

Y en tu prueba, puedes hacer algo como esto:

const person = new Person();
const res = await person.sayMyName();
expect(res).toEqual('my name is dev');

¡Espero que esto ayude a alguien!

  • ¿Cómo afirmamos que el simulacro fue llamado?

    – Blues de Jasper

    02 feb.

Realmente no respondo la pregunta, pero quiero mostrar un caso de uso en el que desea simular una clase dependiente para verificar otra clase.

Por ejemplo: Foo depende de Bar. Internamente Foo creó una instancia de Bar. quieres burlarte Bar para las pruebas Foo.

Clase de barra

class Bar {
  public runBar(): string {
    return 'Real bar';
  }
}

export default Bar;

clase de foo

import Bar from './Bar';

class Foo {
  private bar: Bar;

  constructor() {
    this.bar = new Bar();
  }

  public runFoo(): string {
    return 'real foo : ' + this.bar.runBar();
  }
}

export default Foo;


La prueba:

import Foo from './Foo';
import Bar from './Bar';

jest.mock('./Bar');

describe('Foo', () => {
  it('should return correct foo', () => {
    // As Bar is already mocked,
    // we just need to cast it to jest.Mock (for TypeScript) and mock whatever you want
    (Bar.prototype.runBar as jest.Mock).mockReturnValue('Mocked bar');
    const foo = new Foo();
    expect(foo.runFoo()).toBe('real foo : Mocked bar');
  });
});


Nota: esto no funcionará si usa funciones de flecha para definir métodos en su clase (ya que son diferencias entre instancias). Convertirlo en un método de instancia regular lo haría funcionar.

Ver también jest.requireActual(nombre del módulo)

  • ¿Cómo afirmamos que el simulacro fue llamado?

    – Blues de Jasper

    02 feb.

He combinado las respuestas de @sesamechicken y @Billy Reilly para crear una función útil que se burla de (uno o más) métodos específicos de una clase, sin afectar definitivamente a la clase en sí.

/**
* @CrazySynthax class, a tiny bit updated to be able to easily test the mock.
*/
class Person {
    constructor(first, last) {
        this.first = first;
        this.last = last;
    }

    sayMyName() {
        return this.first + " " + this.last + this.yourGodDamnRight();
    }

    yourGodDamnRight() {
        return ", you're god damn right";
    }
}

/**
 * Return a new class, with some specific methods mocked.
 *
 * We have to create a new class in order to avoid altering the prototype of the class itself, which would
 * most likely impact other tests.
 *
 * @param Klass: The class to mock
 * @param functionNames: A string or a list of functions names to mock.
 * @returns {Class} a new class.
 */
export function mockSpecificMethods(Klass, functionNames) {
    if (!Array.isArray(functionNames))
        functionNames = [functionNames];

    class MockedKlass extends Klass {
    }

    const functionNamesLenght = functionNames.length;
    for (let index = 0; index < functionNamesLenght; ++index) {
        let name = functionNames[index];
        MockedKlass.prototype[name] = jest.fn();
    };

    return MockedKlass;
}

/**
* Making sure it works
*/
describe('Specific Mocked function', () => {
    it('mocking sayMyName', () => {
        const walter = new (mockSpecificMethods(Person, 'yourGodDamnRight'))('walter', 'white');

        walter.yourGodDamnRight.mockReturnValue(", that's correct"); // yourGodDamnRight is now a classic jest mock;

        expect(walter.sayMyName()).toBe("walter white, that's correct");
        expect(walter.yourGodDamnRight.mock.calls.length).toBe(1);

        // assert that Person is not impacted.
        const saul = new Person('saul', 'goodman');
        expect(saul.sayMyName()).toBe("saul goodman, you're god damn right");
    });
});

.

¿Ha sido útil esta solución?