GlaceJS vs MochaJS: independent finalizers

Привет!

В продолжение темы особенностей GlaceJS, в этой статье я расскажу про то, как MochaJS **(не)**обрабатывает множественные вызовы after-хуков.

Рассмотрим пример:

describe("scope", () => {
    it("test", () => {});
    after(() => {
        console.log("After #1");
    });
    after(() => {
        console.log("After #2");
    });
});

И смотрим вывод:

$ mocha proba.js


  scope
    √ test
After #1
After #2


  1 passing (6ms)

Как видим использовать несколько after вполне возможно. Но до тех пор, пока в первом не возникнет исключение:

describe("scope", () => {
    it("test", () => {});
    after(() => {
        console.log("After #1");
        throw new Error("BOOM!");
    });
    after(() => {
        console.log("After #2");
    });
});

И ситуация меняется, второй хук не выполняется:

$ mocha proba.js


  scope
    √ test
After #1
    1) "after all" hook


  1 passing (8ms)
  1 failing

  1) scope
       "after all" hook:
     Error: BOOM!
      at Context.after (proba.js:5:15)

MochaJS обрывает выполнение хуков при первом же исключении. Это приводит к тому, что попытка использовать несколько after как независимые финалайзеры, провалится:

scope("My test", () => {

    before(async () => {
        await startProxy();
        await launchBrowser();
    });

    it("some test #1", async () => {
        await openSomeUrl();
    });

    it("some test #2", async () => {
        await openAnotherUrl();
    });

    after(async () => await closeBrowser());
    after(async () => await stopProxy());
});

Если произойдет исключение в closeBrowser(), то хук c stopProxy() не будет выполнен, и прокси останется висеть. Конечно можно сказать, что лучше выключать и включать прокси в хуке before. Но это скорее workaround, rootcause в том, что MochaJS не поддерживает независимое выполнение after-хуков, как это н-р работает в финализации фикстур в pytest. Но это легко исправить путем патча MochaJS. Вот как это реализуется в GlaceJS:
https://github.com/schipiga/glacejs/blob/master/lib/hacking.js#L87. После чего after-хуки выполняются все, даже если в одном было порождено исключение.

Еще одна проблема, связанная с хуками в том, что если в хуке before происходит исключение, то хук after будет вызван (дефолтное поведение MochaJS). И из-за невыполнения before, в after также может произойти исключение. Это немного портит анализ репортов. В GlaceJS используется подход не выполнять стэп-финализатор, если стэп-инициатор не был выполнен. Н-р:

/* test example */
test("Some test", () => {
    before(async () => {
        await SS.launchBrowser();
    });
    chunk(async () => {
        await SS.openApp();
    });
    after(async () => {
        await SS.closeBrowser();
    });
});

/* steps example */
var WebSteps = {
    /**
     * Step to launch browser. Step recall will be skipped if
     *  browser wasn't closed before.
     *
     * @method
     * @instance
     * @async
     * @arg {object} [opts] - Step options.
     * @arg {boolean} [opts.check=true] - Flag to check that browser was
     *  launched.
     * @throws {AssertionError} - If browser wasn't launched.
     */
    launchBrowser: async function (opts) {

        if (this._isBrowserLaunched) {
            logger.stepDebug("Step to launch browser was passed already");
            return;
        };

        opts = U.defVal(opts, {});
        var check = U.defVal(opts.check, true);

        var hostname = os.hostname().toLowerCase();
        var proxyOptions = [ "ignore-certificate-errors",
                             `proxy-server=${hostname}:${CONF.globalProxyPort}`,
                             `proxy-bypass-list=localhost,127.0.0.1,${hostname}` ];
        var chromeArgs = this._webdriver.desiredCapabilities.chromeOptions.args;

        for (var option of proxyOptions) {
            if (this._isGlobalProxyStarted) {
                if (!chromeArgs.includes(option)) {
                    chromeArgs.push(option);
                };
            } else {
                if (chromeArgs.includes(option)) {
                    var idx = chromeArgs.indexOf(option);
                    chromeArgs.splice(idx, 1);
                };
            };
        };
        await this._webdriver.init();

        if (check) {
            expect(await this._webdriver.session(),
                   "Browser wasn't launched").to.exist;
        };

        this._isBrowserLaunched = true;
    },
    /**
     * Step to close browser. Step will be skipped if browser wasn't launched
     *  before.
     *
     * @method
     * @instance
     * @async
     * @arg {object} [opts] - Step options.
     * @arg {boolean} [opts.check=true] - Flag to check that browser was closed.
     * @throws {AssertionError} - If browser wasn't closed.
     */
    closeBrowser: async function (opts) {

        if (!this._isBrowserLaunched) {
            logger.stepDebug("Step to launch browser wasn't passed yet");
            return;
        };

        opts = U.defVal(opts, {});
        opts.check = U.defVal(opts.check, true);

        await this._webdriver.end();
        await this.pause(1, "webdriver process will be stopped");

        if (opts.check) {
            expect(await this._webdriver.session(),
                   "Browser wasn't closed").to.not.exist;
        };
        this._isBrowserLaunched = false;
    },
}