Preliminaries

You should have set up your package as shown in Setup.

For the course of this tutorial we will be using @testdeck/mocha. So you might want to replace this with your favorite test framework instead.

Also, we will use the expectation framework provided by Chai.js, so that you can easily migrate the provided example code to either @testdeck/jasmine or @testdeck/jest.

Also, the existing examples will focus on TypeScript but we try to keep as simple as possible to allow for easy adoption to for example Babel.

Add Source QueryService

This is our system under test, or as some might call it, the CUT, or class under test.

SUT represents a simple QueryService that runs a query by a QueryExecutor implementation. For testing purposes, we can easily exchange the default implementation by a custom one in order to for example induce errors into the system or return test specific results.

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
export interface QueryExecutor {

    executeQuery(query: {}): Promise<any>;
}

export interface QueryServiceConfig {

  executor?: QueryExecutor;
}

// while this should be testable, too, we will not go to lengths here
class DefaultQueryExecutorImpl implements QueryExecutor {

    public executeQuery(query: {}): Promise<any> {
    
        return Promise.resolve({});
    }
}

export default class QueryService {

  private _isRunning: boolean;
  
  public get isRunning() {
  
    return this._isRunning;
  }
  
  private constructor(private readonly executor: QueryExecutor) {
  
    this._isRunning = false;
  }
  
  public static fromConfig(config: QueryServiceConfig) {
  
    return new QueryService(config.executor || new DefaultQueryExecutorImpl());
  }

  public start() {

    this._isRunning = true;
  }  

  public stop() {

    this._isRunning = false;
  }
  
  public queryAsync1(query: {}, cb: Function) {

    if (!this.isRunning) {
    
      return cb(new Error('not running'), null);
    }
    
    this.executor.executeQuery(query)
      .then((result) => {

        cb(null, result);
      }, (err) => {

        cb(err, null);
      });
  }

  public queryAsync2(query: {}): Promise<any> {

     if (!this.isRunning) {
     
        return Promise.reject(new Error('not running'));
     }

     return this.executor.executeQuery(query);
  }

  public async queryAsync3(query: {}): Promise<any> {

     if (!this.isRunning) {
     
        return Promise.reject(new Error('not running'));
     }

     return this.executor.executeQuery(query);
  }
}

Add Suite QueryServiceTests

Let’s create an empty test suite first.

Please note that we will not develop our SUT in a TDD/BDD way while we implement our tests. This is both to keep things as compact as possible and this not being a tutorial on how to apply TDD/BDD.

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

import QueryService from '../src/QueryService';

@suite
class QueryServiceTests {

}

Add Basic Behaviour Tests

We now add an initial set of tests for the basic behaviour of the SUT.

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
// ...

@suite
class QueryServiceTests {

  // ...

  @test
  isRunningMustReturnFalseByDefault() {
  
     const sut = QueryService.fromConfig({});
     
     expect(sut.isRunning).to.be.false;
  }

  @test
  isRunningMustReturnTrueAfterStartWasCalled() {
  
     const sut = QueryService.fromConfig({});
     
     sut.start();
     expect(sut.isRunning).to.be.true;
  }

  @test
  isRunningMustReturnFalseAfterStopWasCalled() {
  
     const sut = QueryService.fromConfig({});
     
     sut.start();
     sut.stop();
     expect(sut.isRunning).to.be.false;
  }
  
  @test
  fromConfigMustFailOnWronglyConfiguredExecutor(done) {
  
    // well, we lied, there is a FIXME here somewhere...
    try {
    
      const sut = QueryService.fromConfig({
        executor: {
          executeQuery: null
        }
      });
      
      done(new Error('query service failed to detect invalid config'));
    } catch (err) {
    
      done();      
    }
  }
}

Run Tests

npm test

> nyc mocha

  QueryServiceTests
    ✓ isRunningMustReturnFalseByDefault
    ✓ isRunningMustReturnTrueAfterStartWasCalled
    ✓ isRunningMustReturnFalseAfterStopWasCalled
    1) fromConfigMustFailOnWronglyConfiguredExecutor

  3 passing (1s)
  1 failing

  1) QueryServiceTests
       fromConfigMustFailOnWronglyConfiguredExecutor:
     Error: query service failed to detect invalid config
      at QueryServiceTests.fromConfigMustFailOnWronglyConfiguredExecutor (test/QueryServiceTests.ts:48:12)
      at Context.fromConfigMustFailOnWronglyConfiguredExecutor (node_modules/@testdeck/core/dist/index.js:153:39)

Nice, as expected the configuration was invalid and QueryService failed to detect this.

Add #queryAsync1() Behaviour Tests

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
65
66
67
68
69
70
71
72
73
74
75
76
// ...

@suite
class QueryServiceTests {

  // ...

  @test
  queryAsync1MustFailWhenNotStarted(done) {
  
    const sut = QueryService.fromConfig({});
    
    sut.queryAsync1({}, (err, result) => {
    
      try {
      
        expect(result).to.be.null;
        expect(err).to.not.be.null;
        expect(err.toString()).to.contain('not running');
        done();
      } catch (ex) {

        done(ex);  
      }
    });
  }
  
  @test
  queryAsync1MustSucceedWhenStartedAndEverythingWorkingAsExpected(done) {
  
    const sut = QueryService.fromConfig({});
    
    sut.start();
    sut.queryAsync1({}, (err, result) => {
    
      try {
      
        expect(err).to.be.null;
        expect(result).to.not.be.null;
        expect(result).to.deep.equal({});
        done();
      } catch (ex) {

        done(ex);    
      }
    });
  }

  @test
  queryAsync1MustFailOnErrorByExecutor(done) {
  
    const sut = QueryService.fromConfig({
      executor: {
        executeQuery: function(query: {}): Promise<any> {
        
          return Promise.reject(new Error('remote unavailable'));
        }
      }
    });

    sut.start();
    sut.queryAsync1({}, (err, result) => {
    
      try {
      
        expect(result).to.be.null;
        expect(err).to.not.be.null;
        expect(err.toString()).to.contain('remote unavailable');
        done();
      } catch (ex) {

        done(ex);     
      }
    });  
  }
}

Run Tests

npm test

> nyc mocha

...

    ✓ queryAsync1MustFailWhenNotStarted
    ✓ queryAsync1MustSucceedWhenStartedAndEverythingWorkingAsExpected
    ✓ queryAsync1MustFailOnErrorByExecutor

...

Add #queryAsync2() Behaviour Tests

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
// ...

@suite
class QueryServiceTests {

  // ...

  @test
  queryAsync2MustFailWhenNotStarted() {
  
    const sut = QueryService.fromConfig({});
    
    return sut.queryAsync2({})
      .then((result) => {
      
        expect.fail('must not have been called');
      }, (err) => {
      
        expect(err).to.not.be.null;
        expect(err.toString()).to.contain('not running'));
      }); 
  }

  @test
  queryAsync2MustSucceedWhenStartedAndEverythingWorkingAsExpected() { 
  
    const sut = QueryService.fromConfig({});
    
    sut.start();
    return sut.queryAsync2({})
      .then((result) => {
      
        expect(result).to.not.be.null;
        expect(result).to.deep.equal({});
      }, (err) => {
      
        expect.fail('must not have been called');
      }); 
  }
  
  @test
  queryAsync2MustFailOnErrorByExecutor() {
  
    const sut = QueryService.fromConfig({
      executor: {
        executeQuery: function(query: {}): Promise<any> {
        
          return Promise.reject(new Error('remote unavailable'));
        }
      }
    });

    sut.start();
    return sut.queryAsync2({})
      .then((result) => {
      
        expect.fail('must not have been called');
      }, (err) => {
      
        expect(err).to.not.be.null;
        expect(err.toString()).to.contain('remote unavailable'));
      });
  }
}

Run Tests

...

    ✓ queryAsync2MustFailWhenNotStarted
    ✓ queryAsync2MustSucceedWhenStartedAndEverythingWorkingAsExpected
    ✓ queryAsync2MustFailOnErrorByExecutor
    
...

Add #queryAsync3() Behaviour Tests

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
65
// ...

@suite
class QueryServiceTests {

  // ...
  
  @test
  async queryAsync3MustFailWhenNotStarted() {
    
    const sut = QueryService.fromConfig({});
    
    try {
    
      const result = await sut.queryAsync3({});
      
      expect.fail('error expected');
    } catch (err) {
    
      expect(err.toString()).to.contain('not running');
    }
  }

  @test
  async queryAsync3MustSucceedWhenStartedAndEverythingWorkingAsExpected() {
   
    const sut = QueryService.fromConfig({});

    sut.start();    

    try {

      const result = await sut.queryAsync3({});
      
      expect(result).to.deep.equal({});
    } catch (err) {
    
      expect.fail(err.toString());
    }
  }
  
  @test
  async queryAsync3MustFailOnErrorByExecutor() {
  
    const sut = QueryService.fromConfig({
      executor: {
        executeQuery: function(query: {}): Promise<any> {
        
          return Promise.reject(new Error('remote unavailable'));
        }
      }
    });

    sut.start();    

    try {
    
      const result = await sut.queryAsync3({});
      expect.fail('should not have succeeded');
    } catch (err) {
    
      expect(err.toString()).to.contain('remote unavailable');
    }
  }
}

Run Tests

...

    ✓ queryAsync3MustFailWhenNotStarted
    ✓ queryAsync3MustSucceedWhenStartedAndEverythingWorkingAsExpected
    ✓ queryAsync3MustFailOnErrorByExecutor
    
...