Suite Inheritance

In addition to nesting, testdeck supports inheritance. And while you may not inherit from concrete suites, i.e. those that have been decorated with the @suite decorator, you can build yourself test base class hierarchies that provide tests. And, using proper chaining, you can even override existing lifecycle hooks.

Why Limitations On Inheritance?

Deriving from other concrete suites will be detected by testdeck and will be reported as an error.

The rationale behind this is that under the hood, for each @suite annotated class, describe will be called, which then will be passed a callback function that will then do the rest, e.g. register tests, run lifecycle hooks and so on.

Inheriting from another @suite annotated class will run this process twice, once for the super class and a second time for the child class. And this is something that we do not want to happen.

So don’t do this.

1
2
3
4
5
6
7
8
9
import { suite } from '@testdeck/mocha';

@suite
class SuperSuite {
}

@suite
class ChildSuite extends SuperSuite {
}
npm test

...
Error: @suite ChildSuite must not subclass another @suite decorated class, use abstract base classes instead.
...

Putting Inheritance to Test

TypeScript allows you to define abstract classes that you can use for establishing a common test framework for your concrete tests to build upon, so we recommend using these as it is more clean approach to OO than inheriting from a concrete class. Since the ECMAScript language specification does not support abstract classes yet, you are absolutely fine subclassing concrete classes, too. For the course of this guide we will stick to abstract classes, though.

For starters you might want to have a look at our original mocha-typescript test sources. Not so much for the poor performance of the tests, mind you, but from reading through the sources, you might get some ideas on what inheritance can do for you.

Example Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { suite, test } from '@testdeck/mocha';
import { expect } from 'chai';

abstract class AbstractTestBase {

  protected abstract createNewInstance();

  @test
  inheritedTest() {

    expect(this.createNewInstance()).to.deep.equal({ 'foo': 'bar' });
  }
}

@suite
class ConcreteTests extends AbstractTestBase {

  protected createNewInstance() {

    return { 'foo': 'bar' };
  }

  @test
  specificTest() {

    expect(this.createNewInstance().foo).to.contain('bar');
  }
}

Run Tests

npm test

...

  ConcreteTests
    ✓ specificTest
    ✓ inheritedTest
...

Static Lifecycle Hook Chaining

Static lifecycle hooks are inherited. So if your super class provides a static lifecycle hook, then your child class will have that hook and you might want to consider overriding it.

However, chaining static asynchronous lifecycle hooks is not so straight forward as one might think.

Callback Style

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import { suite, test } from '@testdeck/mocha';

abstract class AbstractTestBase {

  static before(done) {

    console.log('AbstractTestBase.before');
    return done();
  }

  static after(done) {

    console.log('AbstractTestBase.after');
    return done(new Error('AbstractTestBase.after failed'), null);
  }
}

@suite
class ConcreteTests extends AbstractTestBase {

  static before(done) {

    // let super before do its thing first
    super.before((err, res) => {

      if (err) {

        return done(err);
      }

      // do our thing
      console.log('ConcreteTest.before');

      return done();
    });
  }

  static after(done) {

    let err;
    try {

      console.log('ConcreteTest.after');

      // do our thing

    } catch (ex) {

      err = ex;
    } finally {

      return super.after((err2, res) => {

        // might want to merge both errors here into one
        return done(err || err2, null);
      });
    }
  }

  @test
  test() {
  }
}

Run Tests

npm test

...

  ConcreteTests
AbstractTestBase.before
ConcreteTest.before
    ✓ test
ConcreteTest.after
AbstractTestBase.after
    1) "after all" hook: after for "test"

...

  1) ConcreteTests
       "after all" hook: after for "test":
     Error: AbstractTestBase.after failed

...

Promise Style

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import { suite, test } from '@testdeck/mocha';

abstract class AbstractTestBase {

  static before(): Promise<any> {

    console.log('AbstractTestBase.before');
    return Promise.resolve({});
  }

  static after(): Promise<any> {

    console.log('AbstractTestBase.after');
    return Promise.reject(new Error('AbstractTestBase.after failed'));
  }
}

@suite
class ConcreteTests extends AbstractTestBase {

  static before(): Promise<any> {

    // let super before do its thing first
    return super.before()
      .then((res) => {

        return new Promise((resolve, reject) => {

          try {

            console.log('ConcreteTest.before');
            // do our thing

            resolve(res);
          } catch (err) {

            reject(err);
          }
        });
      }, (err) => {

        return Promise.reject(err);
      });
  }

  static after(): Promise<any> {

    try {

      console.log('ConcreteTest.after');

      // do our thing

      return super.after();
    } catch (err) {

      return Promise.reject(err);
    }
  }

  @test
  test() {
  }
}

Run Tests

npm test

...

  ConcreteTests
AbstractTestBase.before
ConcreteTest.before
    ✓ test
ConcreteTest.after
AbstractTestBase.after
    1) "after all" hook: after for "test"

...

  1) ConcreteTests
       "after all" hook: after for "test":
     Error: AbstractTestBase.after failed

...

Async/Await Style

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import { suite, test } from '@testdeck/mocha';

abstract class AbstractTestBase {

  static async before(): Promise<any> {

    console.log('AbstractTestBase.before');
    return Promise.resolve(null);
  }

  static async after(): Promise<any> {

    console.log('AbstractTestBase.after');
    return Promise.reject(new Error('AbstractTestBase.after failed'));
  }
}

@suite
class ConcreteTests extends AbstractTestBase {

    static async before(): Promise<any> {

    try {

      await super.before();

      // do our thing
      console.log('ConcreteTest.before');

      return Promise.resolve(null);
    } catch (err) {

      return Promise.reject(null);
    }
  }

  static async after(): Promise<any> {

    try {

      // do our thing
      console.log('ConcreteTest.after');

      await super.after();

      return Promise.resolve(null);
    } catch (err) {

      return Promise.reject(err);
    }
  }

  @test
  test() {
  }
}

Run Tests

npm test

...

  ConcreteTests
AbstractTestBase.before
ConcreteTest.before
    ✓ test
ConcreteTest.after
AbstractTestBase.after
    1) "after all" hook: after for "test"

...

  1) ConcreteTests
       "after all" hook: after for "test":
     Error: AbstractTestBase.after failed

...

Instance Lifecycle Hook Chaining

Instance lifecycle hooks are inherited. So if your super class provides an instance lifecycle hook, then your child class will have that hook and you might want to consider overriding it.

The below code is basically the same as shown in Static Lifecycle Hook Chaining, except for the scope of the hook methods.

Callback Style

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import { suite, test } from '@testdeck/mocha';

abstract class AbstractTestBase {

  before(done) {

    console.log('AbstractTestBase.before');
    return done();
  }

  after(done) {

    console.log('AbstractTestBase#after');
    return done(new Error('AbstractTestBase#after failed'), null);
  }
}

@suite
class ConcreteTests extends AbstractTestBase {

  before(done) {

    // let super before do its thing first
    super.before((err, res) => {

      if (err) {

        return done(err);
      }

      // do our thing
      console.log('ConcreteTest#before');

      return done();
    });
  }

  after(done) {

    let err;
    try {

      console.log('ConcreteTest#after');

      // do our thing

    } catch (ex) {

      err = ex;
    } finally {

      return super.after((err2, res) => {

        // might want to merge both errors here into one
        return done(err || err2, null);
      });
    }
  }

  @test
  test() {
  }
}

Run Tests

npm test

...

  ConcreteTests
AbstractTestBase#before
ConcreteTest#before
    ✓ test
ConcreteTest#after
AbstractTestBase#after
    1) "after each" hook: after for "test"

...

  1) ConcreteTests
       "after each" hook: after for "test":
     Error: AbstractTestBase#after failed

...

Promise Style

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import { suite, test } from '@testdeck/mocha';

abstract class AbstractTestBase {

  before(): Promise<any> {

    console.log('AbstractTestBase#before');
    return Promise.resolve({});
  }

  after(): Promise<any> {

    console.log('AbstractTestBase#after');
    return Promise.reject(new Error('AbstractTestBase#after failed'));
  }
}

@suite
class ConcreteTests extends AbstractTestBase {

  before(): Promise<any> {

    // let super before do its thing first
    return super.before()
      .then((res) => {

        return new Promise((resolve, reject) => {

          try {

            console.log('ConcreteTest#before');
            // do our thing

            resolve(res);
          } catch (err) {

            reject(err);
          }
        });
      }, (err) => {

        return Promise.reject(err);
      });
  }

  after(): Promise<any> {

    try {

      console.log('ConcreteTest#after');

      // do our thing

      return super.after();
    } catch (err) {

      return Promise.reject(err);
    }
  }

  @test
  test() {
  }
}

Run Tests

npm test

...

  ConcreteTests
AbstractTestBase#before
ConcreteTest#before
    ✓ test
ConcreteTest#after
AbstractTestBase#after
    1) "after each" hook: after for "test"

...

  1) ConcreteTests
       "after each" hook: after for "test":
     Error: AbstractTestBase#after failed

...

Async/Await Style

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import { suite, test } from '@testdeck/mocha';

abstract class AbstractTestBase {

  async before(): Promise<any> {

    console.log('AbstractTestBase#before');
    return Promise.resolve(null);
  }

  async after(): Promise<any> {

    console.log('AbstractTestBase#after');
    return Promise.reject(new Error('AbstractTestBase#after failed'));
  }
}

@suite
class ConcreteTests extends AbstractTestBase {

    async before(): Promise<any> {

    try {

      await super.before();

      // do our thing
      console.log('ConcreteTest#before');

      return Promise.resolve(null);
    } catch (err) {

      return Promise.reject(null);
    }
  }

  async after(): Promise<any> {

    try {

      // do our thing
      console.log('ConcreteTest#after');

      await super.after();

      return Promise.resolve(null);
    } catch (err) {

      return Promise.reject(err);
    }
  }

  @test
  test() {
  }
}

Run Tests

npm test

...

  ConcreteTests
AbstractTestBase#before
ConcreteTest#before
    ✓ test
ConcreteTest#after
AbstractTestBase#after
    1) "after each" hook: after for "test"

...

  1) ConcreteTests
       "after each" hook: after for "test":
     Error: AbstractTestBase#after failed

...