Mocha: `beforeEach` et `afterEach` s'exécutent dans des suites/contextes imbriqués

Créé le 28 juin 2013  ·  17Commentaires  ·  Source: mochajs/mocha

Il n'y a aucune documentation sur _quand_ un beforeEach ou afterEach s'exécutera. Mon intuition indique qu'il devrait être exécuté before / after chaque bloc describe / it dans le contexte actuel se termine.

Cependant, le comportement que je remarque est que beforeEach et afterEach sont exécutés before / after _every_ it block dans le contexte actuel et tous les contextes imbriqués.

J'ai créé une preuve de concept pour démontrer que le comportement remarqué se produit : https://gist.github.com/twolfson/5883057#file -test-js

Pour plus de commodité, je vais copier/coller le script et le sortir ici :

Scénario

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!');
    });
  });
});

Sortir

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

Le comportement que j'anticipe intuitivement est afterEach à exécuter une fois, après le second nested it .

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

Mon cas d'utilisation de la fonctionnalité intuitive consiste à nettoyer le contexte this une fois le contexte terminé. L'implémentation actuelle nettoiera le contexte après chaque assertion.

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');
    });
  });
});

Un exemple comparable à celui-ci serait l'ouverture d'une transaction de base de données dans chaque nouveau contexte d'une suite de tests.

Je peux contourner cela en appelant after dans chaque contexte imbriqué, mais j'ai l'impression qu'il pourrait y avoir un crochet dans le framework.

Commentaire le plus utile

Pour tout le monde avec ce scénario commun, j'ai créé cette fonction d'assistance qui parcourt toutes les suites du bloc actuel et ajoute un crochet beforeAll à chacune d'elles :

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)
    })
  })
}

Cela débloque le cas d'utilisation que j'ai décrit il y a presque un an :

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')  
  })
})

Je ne comprends toujours pas pourquoi ce cas d'utilisation n'est pas considéré comme utile ou légitime.
Peut-être que cette solution peut être transformée en un PR pour un beforeEachSuite et un afterEachSuite

Tous les 17 commentaires

Pour les curieux, voici la solution de contournement pour mon cas d'utilisation :

// 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 s'exécute après chaque Runnable instance ; dans votre cas, un bloc it() . Si vous ne voulez pas du comportement imbriqué, n'imbriquez pas vos tests. La modification de ce comportement n'est pas sur la table.

Si votre deuxième test dépend de quelque chose défini dans votre premier test, vos tests sont sales et vous le faites mal. L'ordre dans lequel vos tests s'exécutent ne devrait pas avoir d'importance. Écrivez vos tests en conséquence, et ils seront plus précieux.

De plus, il n'est pas nécessaire de mettre quoi que ce soit sur this 90% du temps.

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-t-il un moyen d'exécuter des méthodes de hook dans le niveau describe() au lieu du niveau it() ? Cela peut être utile dans certains scénarios.

@RathaKM Quel est le cas d'utilisation ?

Je ne sais pas s'il s'agit d'un bon exemple, mais ce qui suit serait-il un bon cas d'utilisation pour cela ? @RathaKM n'hésitez pas à corriger.

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 : merci pour vos commentaires et rapportez-le. :) Votre exemple est ce qui devrait logiquement fonctionner dans le niveau de description (le BeforeEach). Mais, cela pourrait être réalisé avec 'before ()' dans chaque description.

Le problème auquel je faisais face est donné dans l'exemple ci-dessous.

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();
            });
        });
    });
});

Ici, la suite de tests échouera. Nous mettons à jour la méthode hook _testData_ dans 'before()' qui sera appelée juste avant le bloc '_it_'. Mais, nous utilisons la variable _testData_ dans _dataDriven()_ qui sera exécutée avant d'exécuter le bloc '_it_'.

Donc, ce dont nous avons besoin ici, c'est d'un endroit pour mettre à jour la variable avant le début de l'exécution du bloc _'it'_. Peut-être avons-nous besoin de l'exécution de la méthode de crochet _before()_ juste avant le bloc 'describe' (méthode de crochet de niveau de description).

J'espère que le problème est clair maintenant.

Y a-t-il eu d'autres discussions à ce sujet?

pourquoi n'est-il pas possible d'obtenir quelque chose comme ça?

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')  
  })
})
  • Nettoyez la base de données avant chaque bloc context .
  • Configurez le scénario avant tous les blocs it

N'est-ce pas un cas d'utilisation légitime ?

@marqu3z J'essaie de suivre votre cas d'utilisation. C'est légitime. Il semble que le problème soit le suivant : le hook before s'exécute avant le hook beforeEach ? mais vous avez besoin que le crochet avant s'exécute après le crochet avantEach ?

(Voulez-vous vraiment nettoyer la base de données pour chaque scénario de test ? Il semble que seul votre premier scénario de test réussira, les autres n'auront aucune donnée. Mais je suppose que vous souhaitez nettoyer la base de données pour chaque scénario de test).

Voici mes deux tentatives pour résoudre votre cas d'utilisation compte tenu de Mocha tel quel :

(1) utilisez un beforeEach, mais retournez un booléen, donc le beforeEach ne s'exécute qu'une fois (c'est un peu nul)

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) L'autre façon de procéder - ce n'est pas beaucoup mieux !

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){}))  
  })
})

Veuillez me faire savoir si je comprends votre problème, et je passerai plus de cycles à réfléchir à une solution. Aucune de mes solutions n'est très bonne, mais je veux juste m'assurer de bien comprendre le problème que vous rencontrez.

@RathaKM

enregistrez-vous vos cas de test it() de manière asynchrone ? Si c'est le cas, ce n'est pas autorisé. Vous devez enregistrer describe/after/afterEach/before/beforeEach synchrone.

// 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();
       });
  });

aussi, d'où vient ctx ? Peut-être que je ne suis pas au courant des dernières fonctionnalités de Mocha, mais je n'ai jamais vu

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

Je suis exactement dans le même scénario que @marqu3z décrit. J'adorerais voir de l'aide pour un niveau describe beforeEach

Pour tout le monde avec ce scénario commun, j'ai créé cette fonction d'assistance qui parcourt toutes les suites du bloc actuel et ajoute un crochet beforeAll à chacune d'elles :

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)
    })
  })
}

Cela débloque le cas d'utilisation que j'ai décrit il y a presque un an :

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')  
  })
})

Je ne comprends toujours pas pourquoi ce cas d'utilisation n'est pas considéré comme utile ou légitime.
Peut-être que cette solution peut être transformée en un PR pour un beforeEachSuite et un afterEachSuite

Oui, s'il vous plaît, nous en avons besoin.

Il existe de nombreux cas d'utilisation comme https://github.com/mochajs/mocha/issues/911#issuecomment -316736991

+1

+1

Je viens de tomber sur ce fil à la recherche de quelque chose de similaire. Alors à tous ceux qui tombent sur ce post...

J'ai adapté l'approche décrite par @marqu3z à son propre package NPM : mocha-suite-hooks .

Donc, si vous vous trouvez dans une situation où before ne suffit pas et beforeEach c'est trop, essayez beforeSuite .

Le paquet est encore assez brut, donc tout commentaire serait grandement apprécié !

Cette page vous a été utile?
0 / 5 - 0 notes