Mocha: `beforeEach` e` afterEach` estão rodando em suítes / contextos aninhados

Criado em 28 jun. 2013  ·  17Comentários  ·  Fonte: mochajs/mocha

Não há documentação sobre _quando_ um beforeEach ou afterEach será executado. Minha intuição afirma que deve ser executado before / after cada bloco de describe / it no contexto atual completo.

No entanto, o comportamento que estou observando é que beforeEach e afterEach são executados before / after _every_ it bloco no contexto atual e todos os contextos aninhados.

Criei uma prova de conceito para demonstrar que o comportamento observado está ocorrendo: https://gist.github.com/twolfson/5883057#file -test-js

Por conveniência, copiarei / colarei o script e a saída aqui:

Roteiro

describe('An afterEach hook', function () {
  afterEach(function () {
    console.log('afterEach run!');
  });

  before(function () {
    console.log('before run!');
  });

  describe('in some nested contexts', function () {
    before(function () {
      console.log('nested before run!');
    });

    it('runs after this block', function () {
      console.log('nested it run!');

    });

    it('runs again after this block', function () {
      console.log('second nested it run!');
    });
  });
});

Saída

before run!
nested before run!
nested it run!
afterEach run!
second nested it run!
afterEach run!

O comportamento que antecipo intuitivamente é afterEach para ser executado uma vez, após second nested it .

before run!
nested before run!
nested it run!
second nested it run!
afterEach run!

Meu caso de uso para a funcionalidade intuitiva é limpar o contexto this após a conclusão de um contexto. A implementação atual limpará o contexto após cada afirmação.

afterEach(function () {
  var key;
  for (key in this) {
    delete this[key];
  }
});

describe('A banana', function () {
  before(function () {
    this.banana = new Banana();
  });

  describe('when peeled', function () {
    before(function () {
      this.peeledBanana = this.banana.peel();
    });

    it('is white', function () {
      assert.strictEqual(this.peeledBanana.color, 'white');
      // `afterEach` is invoked here, cleaning out `this`
    });

    it('is soft', function () {
      // `this.peeledBanana` is no longer defined since `afterEach` cleaned it out
      assert.strictEqual(this.peeledBanana.hardness, 'soft');
    });
  });
});

Um exemplo comparável a este seria abrir uma transação de banco de dados em cada novo contexto de um conjunto de testes.

Posso contornar isso invocando after em cada contexto aninhado, mas sinto que pode haver um gancho na estrutura.

Comentários muito úteis

Para todos com este cenário comum, eu criei esta função auxiliar que itera sobre todos os conjuntos no bloco atual e adicionei um gancho beforeAll a cada um deles:

function beforeEachSuite (fn) {
  before(function () {
    let suites = this.test.parent.suites || []
    suites.forEach(s => {
      s.beforeAll(fn)
      let hook = s._beforeAll.pop()
      s._beforeAll.unshift(hook)
    })
  })
}

Isso desbloqueia o caso de uso que descrevo há quase um ano:

describe('Create Article', () => {
  beforeEachSuite(() => console.log('CLEAN DB'))

  context('When Article is valid', () => {
    before(() => console.log("INSERTING VALID ARTICLE"))

    it('should not fail')
    it('should have some properties')    
    it('should send an email')  
  })

  context('When Article is not valid', () => {
    before(() => console.log("INSERTING NOT VALID ARTICLE"))

    it('should fail')
    it('should not send an email')  
  })
})

Ainda não entendo por que esse caso de uso não é considerado útil ou legítimo.
Talvez esta solução possa ser transformada em um PR para beforeEachSuite e afterEachSuite

Todos 17 comentários

Para os curiosos, esta é a solução alternativa para meu caso de uso:

// Before anything else is run
before(function () {
  // Iterate over all of the test suites/contexts
  this.test.parent.suites.forEach(function bindCleanup (suite) {
    // Attach an afterAll listener that performs the cleanup
    suite.afterAll(function cleanupContext () {
      var key;
      for (key in this) {
        delete this[key];
      }
    });
  });
});

+1

afterEach é executado após cada Runnable instância; no seu caso, um bloco it() . Se você não quiser o comportamento aninhado, não aninhe seus testes. Alterar esse comportamento não está na mesa.

Se seu segundo teste depende de algo definido em seu primeiro teste, seus testes estão sujos e você está fazendo isso errado. A ordem em que seus testes são executados não deve importar. Escreva seus testes de acordo e eles serão mais valiosos.

Além disso, não é necessário colocar nada em this 90% do tempo.

describe('A banana', function () {
  var banana;

  before(function () {
    banana = new Banana();
  });

  describe('when peeled', function () {
    var peeledBanana;

    beforeEach(function () {
      // I'm assuming peel() has no side-effects since it returns a new object.
      peeledBanana = banana.peel();
    });

    it('is white', function () {
      assert.strictEqual(peeledBanana.color, 'white');
    });

    it('is soft', function () {
      assert.strictEqual(peeledBanana.hardness, 'soft');
    });
  });
});

Existe uma maneira de executar métodos de gancho no nível describe () em vez do nível it ()? Pode ser útil em alguns cenários.

@RathaKM Qual é o caso de uso?

Não tenho certeza se este é um bom exemplo, mas o seguinte seria um bom caso de uso para isso? @RathaKM sinta-se à vontade para corrigir.

describe('A banana', function () {
  var banana;

  beforeEach(function () {
    banana = new Banana();
  });

  describe('when peeled', function () {
    var peeledBanana;

    before(function () {
      // Lets assume peel() HAS side-effects and doesn't return a new object.
      banana.peel();
      peeledBanana = banana;
    });

    it('is white', function () {
      assert.strictEqual(peeledBanana.color, 'white');
    });

    it('is soft', function () {
      assert.strictEqual(peeledBanana.hardness, 'soft');
    });
  });

  describe('when set on FIRE', function () {
    var flamingBanana;

    before(function () {
      // Same assumption as above
      banana.setOnFire();
      flamingBanana = banana
    });

    it('is hot', function () {
      assert.isAbove(flamingBanana.temperature, 9000);
    });
  });
});

@cpoonolly : obrigado por seus comentários e traga-o de volta. :) Seu exemplo é o que logicamente esperava funcionar no nível de descrição (BeforeEach). Mas, isso pode ser alcançado com 'antes ()' dentro de cada descrição.

O problema que eu estava enfrentando é fornecido no exemplo abaixo.

var testData = [{country: 'NIL'}],
dataDriven = require('data-driven'),
assert = require('assert');

describe('@Testing_For_Describe_Level_Hook_Method_To_Update_TestData@', function () {

    describe('<strong i="8">@Updated</strong> testData for US@', function () {
        before(function (done) {
            testData = [{country: 'US'}];
            done();
        });
        dataDriven(testData, function () {
            it('expecting updated testData for US', function (ctx, done) {
                assert.equal(ctx.country, 'US');
                done();
            });
        });
    });

    describe('<strong i="9">@Updated</strong> testData for UK@', function () {
        before(function (done) {
            testData = [{country: 'UK'}];
            done();
        });
        dataDriven(testData, function () {
            it('expecting updated testData for UK', function (ctx, done) {
                assert.equal(ctx.country, 'UK');
                done();
            });
        });
    });
});

Aqui, o conjunto de testes falhará. Nós atualizamos o _testData_ dentro do método de gancho 'before ()' que será chamado logo antes do bloco '_it_'. Mas, usamos a variável _testData_ dentro de _dataDriven () _ que será executada antes de executar o bloco '_it_'.

Portanto, o que precisamos aqui é um local para atualizar a variável antes do início da execução do bloco _'it'_. Podemos precisar da execução do método de gancho _before () _ antes do bloco 'descrever' (método de gancho de nível de descrição).

Espero, o problema está claro agora.

Houve alguma discussão adicional sobre isso?

por que não é possível obter algo assim?

describe('Create Article', () => {
  beforeEach(() => console.log('CLEAN DB'))

  context('When Article is valid', () => {
    before(() => console.log("INSERTING VALID ARTICLE"))

    it('should not fail')
    it('should have some properties')    
    it('should send an email')  
  })

  context('When Article is not valid', () => {
    before(() => console.log("INSERTING NOT VALID ARTICLE"))

    it('should fail')
    it('should not send an email')  
  })
})
  • Limpe o banco de dados antes de cada bloco context .
  • Configure o cenário antes de todos os blocos it

Não é um caso de uso legítimo?

@ marqu3z Estou tentando acompanhar seu caso de uso. É legítimo. Parece que o problema é: o hook before é executado antes do hook beforeEach? mas você precisa que o gancho before seja executado após o gancho beforeEach?

(você realmente deseja limpar o banco de dados para cada caso de teste? parece que apenas seu primeiro caso de teste terá êxito, os outros não terão dados. Mas presumo que você deseja limpar o banco de dados para cada caso de teste).

Aqui estão minhas duas tentativas de resolver seu caso de uso, dado o Mocha como está:

(1) use beforeEach, mas inverta um booleano, para que beforeEach execute apenas uma vez (isso é meio ruim)

describe('Create Article', () => {
  beforeEach(() => console.log('CLEAN DB'))

  context('When Article is valid', () => {
    let flip = true;
    beforeEach(() => flip && (flip = false; console.log('INSERTING VALID ARTICLE')))

    it('should not fail')
    it('should have some properties')    
    it('should send an email')  
  })

  context('When Article is not valid', () => {
   let flip = true;
    beforeEach(() => flip && (flip = false; console.log('INSERTING NOT VALID ARTICLE')))

    it('should fail')
    it('should not send an email')  
  })
})

(2) A outra maneira de fazer isso - não é muito melhor!

describe('Create Article', () => {

  let cleanDB = function(fn){
    return function(done){
        actuallyCleanDB(function(err){
            if(err) return done(err);
            fn(done);
       });
    }
  };

  context('When Article is valid', () => {
    before(() => console.log('INSERTING VALID ARTICLE'))
    it('should not fail', cleanDB(function(done){})
    it('should have some properties', cleanDB(function(done){}))    
    it('should send an email', cleanDB(function(done){})) 
  })

  context('When Article is not valid', () => {
    before(() => console.log('INSERTING NOT VALID ARTICLE')))
    it('should fail', cleanDB(function(done){}))
    it('should not send an email',cleanDB(function(done){}))  
  })
})

Avise-me se entendi seu problema e passarei mais ciclos pensando em uma solução. Nenhuma das minhas soluções é muito boa, mas só quero ter certeza de que entendi o seu problema.

@RathaKM

você está registrando seus casos de teste it () de forma assíncrona? Nesse caso, isso não é permitido. Você deve registrar describe/after/afterEach/before/beforeEach tudo de forma síncrona.

// event loop tick X
 dataDriven(testData, function () {
     // it() must be called in the same event loop tick as X above
        it('expecting updated testData for UK', function (ctx, done) {
            assert.equal(ctx.country, 'UK');
            done();
       });
  });

Além disso, de onde vem o ctx? Talvez eu não esteja atualizado sobre os recursos mais recentes do Mocha, mas nunca vi

it(foo, function(ctx, done){
   // where is ctx coming from?
});

Estou exatamente no mesmo cenário descrito por @ marqu3z . Adoraria ver o suporte para um nível describe beforeEach

Para todos com este cenário comum, eu criei esta função auxiliar que itera sobre todos os conjuntos no bloco atual e adicionei um gancho beforeAll a cada um deles:

function beforeEachSuite (fn) {
  before(function () {
    let suites = this.test.parent.suites || []
    suites.forEach(s => {
      s.beforeAll(fn)
      let hook = s._beforeAll.pop()
      s._beforeAll.unshift(hook)
    })
  })
}

Isso desbloqueia o caso de uso que descrevo há quase um ano:

describe('Create Article', () => {
  beforeEachSuite(() => console.log('CLEAN DB'))

  context('When Article is valid', () => {
    before(() => console.log("INSERTING VALID ARTICLE"))

    it('should not fail')
    it('should have some properties')    
    it('should send an email')  
  })

  context('When Article is not valid', () => {
    before(() => console.log("INSERTING NOT VALID ARTICLE"))

    it('should fail')
    it('should not send an email')  
  })
})

Ainda não entendo por que esse caso de uso não é considerado útil ou legítimo.
Talvez esta solução possa ser transformada em um PR para beforeEachSuite e afterEachSuite

Sim, por favor, precisamos disso.

Existem muitos casos de uso como https://github.com/mochajs/mocha/issues/911#issuecomment -316736991

+1

+1

Eu apenas tropecei neste tópico procurando por algo semelhante. Então, para qualquer pessoa que tropeçar neste post ...

Eu adaptei a abordagem que @ marqu3z descreveu em seu próprio pacote NPM: mocha-suite-hooks .

Portanto, se você se encontrar em uma situação em que before não é suficiente e beforeEach é demais, experimente beforeSuite .

O pacote ainda está bem cru, então qualquer feedback será muito apreciado!

Esta página foi útil?
0 / 5 - 0 avaliações