Search code examples
unit-testingtestingjestjssinon

sandbox.restore() won't reset stub's called count


I'm completely new to Sinon/Jest and unit testing, so I'm kinda lost here. I tried to make a sandbox to declare all my stubs inside it but even after using sandbox.restore() the stub's call count is preserved, so my test fails in the next 'it'.

I wasn't able to stub TypeORM's objects directly, so I decided to create fake objects with only the methods I needed and made TypeORM's getRepository() use my created objects.

I'm not sure if this approach is even correct but looks like my tests are working, I can assert the number of calls and it's parameters, in the second 'it' I can expect that the error thrown equals 'Email já cadastrado' and if I change the expected message the test fails.

The issue is that the number of calls won't reset before the next 'it' block. So I always get an error on the line "sandbox.assert.calledOnceWithExactly(fakeConnection.getRepository, Cidades)" since it's being called twice (once on previous 'it' and a second time on the current block).

If I remove that line I get an error in the assert transaction section, since I expect commitTransaction to not have been called but it was called once (in the previous 'it' block).

Image of my Error

Here is my test:

const sandbox = sinon.createSandbox();

// Simulating TypeORM's QueryRunner
const fakeQueryRunner = {
    connect: sandbox.stub().returnsThis(),      
    startTransaction: sandbox.stub().returnsThis(),
    rollbackTransaction: sandbox.stub().returnsThis(),
    commitTransaction: sandbox.stub().returnsThis(),
    release: sandbox.stub().returnsThis(),
    manager: { save: sandbox.stub().returnsThis() },
};

// Simulating TypeORM's Connection
const fakeConnection = {
    createQueryRunner: sandbox.stub().returns( fakeQueryRunner ),
    getRepository: sandbox.stub().returnsThis(),
    findOneOrFail: sandbox.stub().returnsThis(),
}

// I've hidden the mock of my parameters/entities (Usuarios, Senhas, Cidades) since I don't think it's needed to solve the problem.

describe('UserRepository', function () {
    let userRepository;

    beforeEach(async () => {
        const module = await Test.createTestingModule({
            providers: [
                UserRepository,
            ],
        }).compile();
        userRepository = module.get<UserRepository>(UserRepository);
    });

    describe('signUp', function () {
        beforeEach(function () {
            // typeORM.getConnection() returns my object simulating a Connection.
            sandbox.stub(typeorm, 'getConnection').returns( fakeConnection as unknown as typeorm.Connection )

            // Stubbing this.create()
            sandbox.stub(userRepository, 'create').returns( Usuarios )
        });
        
        afterEach(function () {
            sandbox.restore();
        });

        it('successfully signs up the user', async function () {
            // Simulating sucessful save transaction
            fakeQueryRunner.manager.save.onCall(0).resolves(Usuarios);
            fakeQueryRunner.manager.save.onCall(1).resolves(Senhas);
            
            // Calling my method
            await userRepository.signUp(mockCredentialsDto);
            
            // First interation, this line works
            sandbox.assert.calledOnceWithExactly(fakeConnection.getRepository, Cidades);

            // Asserting that transaction was commited
            sandbox.assert.calledOnce(fakeQueryRunner.commitTransaction);
            sandbox.assert.notCalled(fakeQueryRunner.rollbackTransaction);
        });

        it('throws a conflic exception as username already exists', async function () {
            // Simulating a reject from transaction
            fakeQueryRunner.manager.save.onCall(0).rejects({ code: '23505' });

            // Calling my method and catching error
            try {
                await userRepository.signUp(mockCredentialsDto)
            } catch (err) {
                expect(err).toEqual(new Error('Email já cadastrado'));
            }

            // Second interation, this line giver ERROR (method called twice)              
            sandbox.assert.calledOnceWithExactly(fakeConnection.getRepository, Cidades);

            // Asserting that transaction was rolled back, this also gives an error since commitTransaction was called once (in the first 'it' block)
            sandbox.assert.notCalled(fakeQueryRunner.commitTransaction);
            sandbox.assert.calledOnce(fakeQueryRunner.rollbackTransaction);
        });
    // I will make another describe block here eventually
});

Here is the method I'm testing:

async signUp(authCredentialsDto: AuthCredentialsDto): Promise<signUpMessage> {
    const { nome, email, genero, dataNascimento, profissao, organizacao, atuacao, nomeCidade, uf, senha } = authCredentialsDto;
    const connection = getConnection();
    const user = this.create();
    const senhas = new Senhas;
    const cidade = await connection.getRepository(Cidades).findOneOrFail({where: { nome: nomeCidade, uf: uf } });

    // Set values
    user.nome = nome;
    user.email = email;
    user.genero = genero;
    user.dataNascimento = dataNascimento;
    user.profissao = profissao;
    user.organizacao = organizacao;
    user.atuacao = atuacao;
    user.idCidade = cidade;
    const salt = await bcrypt.genSalt();
    senhas.senha = await this.hashPassword(senha, salt)
    senhas.idUsuario2 = user;
    
    // Make a transaction to save the data
    const queryRunner = connection.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();
    
    try {
        await queryRunner.manager.save(user);
        await queryRunner.manager.save(senhas);
        await queryRunner.commitTransaction();
    } catch (error) {
        if ( error.code === '23505' ) { // Usuário repetido
            await queryRunner.rollbackTransaction();
            throw new ConflictException('Email já cadastrado')
        } else {
            await queryRunner.rollbackTransaction();
            throw new InternalServerErrorException;
        }
    } finally {
        await queryRunner.release();
    }

    let success: signUpMessage;
    return success;
}

Solution

  • Managed to make it work, I declared my Sinon Sandbox and Objects inside a beforeEach instead of the start of my test.

    My test looks like this now:

    describe('UserRepository', () => {
        let userRepository
        let sandbox
        let fakeQueryRunner
        let fakeConnection
        let mockUser: Usuarios
    
        beforeEach(async () => {
            sandbox = sinon.createSandbox()
    
            // Cria um objeto QueryRunner fake
            fakeQueryRunner = {
                connect: sandbox.stub().returnsThis(),
                startTransaction: sandbox.stub().returnsThis(),
                rollbackTransaction: sandbox.stub().returnsThis(),
                commitTransaction: sandbox.stub().returnsThis(),
                release: sandbox.stub().returnsThis(),
                manager: { save: sandbox.stub().returnsThis() },
            }
    
            // Cria um objeto Connection fake (note que o stub de createQueryRunner retorna o objeto fakeQueryRunner )
            fakeConnection = {
                createQueryRunner: sandbox.stub().returns(fakeQueryRunner),
                getRepository: sandbox.stub().returnsThis(),
                findOneOrFail: sandbox.stub().returnsThis(),
            }
    
            mockUser = {
                idUsuario: '1',
                atuacao: 'lol',
                dataNascimento: '10/10/2021',
                email: 'teste@kik.com',
                genero: Genero.MASCULINO,
                nome: 'Teste Nome',
                organizacao: 'Teste org',
                profissao: 'Teste Prof',
                idCidade: mockCidade,
                membros: [],
                senhas: mockSenha,
                validatePassword: sandbox.stub().returnsThis(),
            }
    
            const module = await Test.createTestingModule({
                providers: [UserRepository],
            }).compile()
            userRepository = module.get<UserRepository>(UserRepository)
        })
    
        afterEach(() => {
            sandbox.restore()
        })
    
        describe('signUp', () => {
            beforeEach(function () {
                // Cria um método falso que retorna o objeto fakeConnection
                sandbox.stub(typeorm, 'getConnection').returns((fakeConnection as unknown) as typeorm.Connection)
                // Simula o Create de UserRepository
                sandbox.stub(userRepository, 'create').returns(Usuarios)
            })
    
            it('successfully signs up the user', async () => {
                // Salvar Usuário e Senha foi bem sucedido
                fakeQueryRunner.manager.save.onCall(0).resolves(Usuarios)
                fakeQueryRunner.manager.save.onCall(1).resolves(Senhas)
    
                await userRepository.signUp(mockCredentialsDto)
    
                // Verificando instanciação do repositório
                const call1 = fakeConnection.getRepository.onCall(0).resolves(Cidades)
                sandbox.assert.calledWith(call1, Cidades)
                sandbox.assert.calledOnceWithExactly(fakeConnection.getRepository, Cidades)
    
                // Verificando Transactions
                sandbox.assert.calledOnce(fakeQueryRunner.commitTransaction)
                sandbox.assert.notCalled(fakeQueryRunner.rollbackTransaction)
            })
    
            it('throws a conflic exception as username already exists', async () => {
                // Houve um erro em um dos saves, resultando num rollback da transaction
                fakeQueryRunner.manager.save.onCall(0).resolves(Usuarios)
                fakeQueryRunner.manager.save.onCall(1).rejects({ code: '23505' })
    
                try {
                    await userRepository.signUp(mockCredentialsDto)
                } catch (err) {
                    expect(err).toEqual(new Error('Email já cadastrado'))
                }
    
                // Verificando instanciação do repositório
                const call1 = fakeConnection.getRepository.onCall(0).resolves(Cidades)
                sandbox.assert.calledWith(call1, Cidades)
                sandbox.assert.calledOnceWithExactly(fakeConnection.getRepository, Cidades)
    
                // Verificando Transactions
                sandbox.assert.notCalled(fakeQueryRunner.commitTransaction)
                sandbox.assert.calledOnce(fakeQueryRunner.rollbackTransaction)
            })
        })
    })