1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

SAP Cloud SDK for JavaScriptを使ってみる (3)テストを書く

Last updated at Posted at 2021-06-25

##はじめに
この記事は、SAP Cloud SDK for JavaScriptを使ってみるシリーズの3回目です。

SAP Cloud SDK for JavaScriptのプロジェクトはNestJSをベースに作られており、NestJSはJestSupsertestを使用したテストの仕組みを備えています。この仕組みを利用したUnit Test、およびe2e(end-to-end) Testを書いてみるというのが今回のテーマです。

※NestJSでのテストについては、以下の動画がわかりやすかったです。
NestJS Testing Tutorial | Unit and Integration Testing

##テストのための設定
NestJSでは、テストのためあらかじめ以下の設定が用意されています。
###ソース
末尾に.spec.tsとつくものがテスト用のソースです。srcフォルダにあるものがUnit Test用、testフォルダにあるe2e-spec.tsとつくものがe2e Test用です。
image.png

###スクリプト
package.jsonにはテスト用のスクリプトが定義されています。今回、Unit Testではtestまたはtest:watch、e2e Testではtest:e2eを使用します。
test:watchは、テスト対象のファイルが変更されると自動的にテストを再実行してくれます。

package.json
  "scripts": {
    ...
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json",
    ...
    "ci-integration-test": "jest --ci --config ./test/jest-e2e.json",
    "ci-backend-unit-test": "jest --ci"
  },

npm run testを実行してみると、以下のように結果が表示されます。
BusinessPartnerControllerのDependencyが解決できなかったというエラーになっていますが、これについては後ほど対応します。

 PASS  src/app.controller.spec.ts (13.177 s)
 PASS  src/business-partner/business-partner.service.spec.ts (15.342 s)
 FAIL  src/business-partner/business-partner.controller.spec.ts (15.383 s)
  ● BusinessPartnerController › should be defined

    Nest can't resolve dependencies of the BusinessPartnerController (?). Please make sure that the argument BusinessPartnerService at index [0] is available in the RootTestModule context.
...
---------------------------------|---------|----------|---------|---------|-------------------
File                             | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
---------------------------------|---------|----------|---------|---------|-------------------
All files                        |   52.17 |        0 |   42.86 |   47.06 |                   
 src                             |      50 |        0 |      75 |      45 |                   
  app.controller.ts              |     100 |      100 |     100 |     100 |                   
  app.module.ts                  |       0 |      100 |     100 |       0 | 1-12              
  app.service.ts                 |     100 |      100 |     100 |     100 |                   
  main.ts                        |       0 |        0 |       0 |       0 | 1-8
 src/business-partner            |      55 |      100 |       0 |      50 | 
  business-partner.controller.ts |      75 |      100 |       0 |   66.67 | 8,13
  business-partner.module.ts     |       0 |      100 |     100 |       0 | 1-9
  business-partner.service.ts    |   83.33 |      100 |       0 |      75 | 7
---------------------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 failed, 2 passed, 3 total
Tests:       1 failed, 2 passed, 3 total
Snapshots:   0 total
Time:        26.452 s

npm run test:e2eを実行すると、以下のように結果が表示されます。こちらは正常終了しました。

 PASS  test/app.e2e-spec.ts (12.101 s)
  AppController (e2e)
    √ / (GET) (821 ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------|---------|----------|---------|---------|-------------------
All files |       0 |        0 |       0 |       0 | 
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        12.366 s
Ran all test suites.

テストを実行すると、s4hana_pipeline/reports配下に実行結果のxmlファイルができます。
/backend-unitの下にあるのがUnite Testの結果で、/backend-integrationの下にあるのがe2e Testの結果です。xmlファイルを直接見てもあまりピンときませんが、CI/CDのパイプラインの中で実行するとテスト結果を見やすい形にして表示してくれます。これについては、次の回で紹介したいと思います。
image.png

以降では、実際にUnit Test, Integration Testをどのように書くかを見ていきます。

##Unit Test
###Controllerのテスト
business-partner.controller.tsの処理をbusiness-partner.controller.spec.tsでテストします。
image.png
business-partner.controller.spec.tsのソースは以下のようになっています。

import { Test, TestingModule } from '@nestjs/testing';
import { BusinessPartnerController } from './business-partner.controller';

describe('BusinessPartnerController', () => {
  let controller: BusinessPartnerController;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [BusinessPartnerController],
    }).compile();

    controller = module.get<BusinessPartnerController>(
      BusinessPartnerController,
    );
  });

  it('should be defined', () => {
    expect(controller).toBeDefined();
  });
});

この状態でテストを実行すると、以下のエラーになりました。これの意味するところは、BusinessPartnerControllerが必要としているBusinessPartnerServiceが見つからないということです。

Nest can't resolve dependencies of the BusinessPartnerController (?). Please make sure that the argument BusinessPartnerService at index [0] is available in the RootTestModule context.

そこで、BusinessPartnerServiceをインポートし、createTestingModuleメソッドにproviderとして渡すようにします。

import { Test, TestingModule } from '@nestjs/testing';
import { BusinessPartnerService } from './business-partner.service'; //追加
import { BusinessPartnerController } from './business-partner.controller';

describe('BusinessPartnerController', () => {
  let controller: BusinessPartnerController;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [BusinessPartnerController],
      providers: [BusinessPartnerService], //追加
    }).compile();

    ...
});

これでエラーがなくなりました。
ただし、この状態だとControllerのテストをしたいのに、テストがServiceに依存することになります。Controller単体のテストができることが望ましいので、BusinessPartnerServiceをモックの実装( mockBusinessPartnerService)に置き換えます。

describe('BusinessPartnerController', () => {
  let controller: BusinessPartnerController;
  const mockBusinessPartnerService = {}; //モックのBusinessPartnerService:このあと実装

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [BusinessPartnerController],
      providers: [BusinessPartnerService],
    })
    //モックに置き換える
    .overrideProvider(BusinessPartnerService)
    .useValue(mockBusinessPartnerService)    
    .compile();

####Business Partner取得のテスト
ControllerのgetAllBusinessPartnersメソッドをテストします。テストすることはシンプルに、BusinessPartnerServiceのgetAllBusinessPartnersを呼べていることです。

テスト対象の処理
  @Get()
  getAllBusinessPartners(): Promise<BusinessPartner[]> {
    return this.businessPartnerService.getAllBusinessPartners();
  }

mockBusinessPartnerServicegetAllBusinessPartnersメソッドを追加します。jest.fn().mockImplementation(value)はvalueに設定した値をPromiseで返すモックファンクションです。

  //モックの返り値
  const mockBusinessPartners = [{
    businessPartner: "1003764",
    firstName: "John",
    lastName: "Doe"
  },
  {
    businessPartner: "1003765",
    firstName: "Jane",
    lastName: "Roe"
  }];
  const mockBusinessPartnerService = {
    //モックのメソッド実装
    getAllBusinessPartners: jest.fn().mockResolvedValue(mockBusinessPartners)
  };

以下でBusiness Partner取得のテストを追加します。jestの2つのメソッドを使ってテストをしています。

  it('should be defined', () => {
    expect(controller).toBeDefined();
  });

  //追加
  it('should get business partners', async () => {
    const result = await controller.getAllBusinessPartners();
    expect(result).toEqual(mockBusinessPartners);
    expect(mockBusinessPartnerService.getAllBusinessPartners).toHaveBeenCalled();
  })

テストは以下のように正常終了します。

 PASS  src/business-partner/business-partner.controller.spec.ts
  BusinessPartnerController
    √ should be defined (15 ms)
    √ controller: should get business partners (4 ms)

---------------------------------|---------|----------|---------|---------|-------------------
File                             | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
---------------------------------|---------|----------|---------|---------|-------------------
All files                        |   28.26 |        0 |   28.57 |   26.47 |                   
 src                             |       0 |        0 |       0 |       0 |                   
  app.controller.ts              |       0 |      100 |       0 |       0 | 1-10              
  app.module.ts                  |       0 |      100 |     100 |       0 | 1-12              
 PASS  src/business-partner/business-partner.controller.spec.ts (11.802 s)
  BusinessPartnerController
    √ should be defined (18 ms)
    √ should get business partners (4 ms)

---------------------------------|---------|----------|---------|---------|-------------------
File                             | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
---------------------------------|---------|----------|---------|---------|-------------------
All files                        |   28.26 |        0 |   28.57 |   26.47 |                   
 src                             |       0 |        0 |       0 |       0 |                   
  app.controller.ts              |       0 |      100 |       0 |       0 | 1-10              
  app.module.ts                  |       0 |      100 |     100 |       0 | 1-12              
  app.service.ts                 |       0 |      100 |       0 |       0 | 1-6               
  main.ts                        |       0 |        0 |       0 |       0 | 1-8               
 src/business-partner            |      65 |      100 |   66.67 |   64.29 | 
  business-partner.controller.ts |     100 |      100 |     100 |     100 | 
  business-partner.module.ts     |       0 |      100 |     100 |       0 | 1-9
  business-partner.service.ts    |   83.33 |      100 |       0 |      75 | 7
---------------------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        17.02 s
Ran all test suites matching /business-partner.controller/i.

Watch Usage: Press w to show more.

####テスト対象のファイルを絞る
特定の機能のみテストしたいときはWatch Usage: Press w to show more.の後にpと打って、patternの後にテスト対象のファイル名(またはその一部)を入力します。

Pattern Mode Usage
 › Press Esc to exit pattern mode.
 › Press Enter to filter by a filenames regex pattern.

 pattern › business-partner.controller

###Serviceのテスト
business-partner.service.tsの処理をbusiness-partner.service.spec.tsでテストします。
image.png

ServiceのgetAllBusinessPartnersメソッドをテストします。ここでは、実際のBusiness Partnerサービスに接続してデータを取得できることを確認します。

テスト対象の処理
  getAllBusinessPartners(): Promise<BusinessPartner[]> {
    return BusinessPartner.requestBuilder()
    .getAll()
    .select(
        BusinessPartner.BUSINESS_PARTNER,
        BusinessPartner.FIRST_NAME,
        BusinessPartner.LAST_NAME,
    )
    .filter(
        BusinessPartner.BUSINESS_PARTNER_CATEGORY.equals('1')
    )
    .execute({
      destinationName: 'MockServer',
    });
  }

business-partner.service.spec.tsを以下のように実装します。mockBusinessPartnersには実際のサービスから返ってくる結果と同じものを設定します。

import { Test, TestingModule } from '@nestjs/testing';
import { BusinessPartnerService } from './business-partner.service';

describe('BusinessPartnerService', () => {
  let service: BusinessPartnerService;

  const mockBusinessPartners = [
    {
      businessPartner: "1003764",
      firstName: "John",
      lastName: "Doe"
    },
    {
      businessPartner: "1003765",
      firstName: "Jane",
      lastName: "Roe"
    },
    {
      businessPartner: "1003766",
      firstName: "John",
      lastName: "Smith"
    },
    {
      businessPartner: "1003767",
      firstName: "Carla",
      lastName: "Coe"
    }
  ];

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [BusinessPartnerService],
    }).compile();

    service = module.get<BusinessPartnerService>(BusinessPartnerService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  it('should get business partners', async () => {
    return expect(service.getAllBusinessPartners()).resolves.toEqual(mockBusinessPartners);
  });
});

この状態でテストを実行すると、以下のエラーになります。"MockServer"というDestinationが見つからないというエラーです。

 FAIL  src/business-partner/business-partner.service.spec.ts (9.678 s)
  BusinessPartnerService
    √ should be defined (13 ms)
    × should get business partners (92 ms)

  ● BusinessPartnerService › should get business partners

    expect(received).resolves.toEqual()

    Received promise rejected instead of resolved
    Rejected to value: [ErrorWithCause: Could not find a destination with name "MockServer"! Unable to execute request.] 

ローカル実行用のDestinationは.envファイルで設定していますが、テストの際には.envファイルを見てくれないようです。
このため、SAP Cloud SDKでは@sap-cloud-sdktest-utilというパッケージを使ってテスト用のDestinationを設定します。

####テスト用Destinationの設定
以下のステップでテスト用Destinationを設定します。

  • @sap-cloud-sdk/test-utilからmockTestDestinationをインポートし、mockTestDestination()を呼ぶ
import { Test, TestingModule } from '@nestjs/testing';
import { BusinessPartnerService } from './business-partner.service';
import { mockTestDestination } from '@sap-cloud-sdk/test-util';

describe('BusinessPartnerService', () => {
  mockTestDestination('MockServer');
  • プロジェクト直下のsystems.jsonファイルでDestinationを設定
systems.json
{
  "systems": [{
    "alias": "MockServer",
    "uri": "https://odata-mock-server-forgiving-hartebeest.cfapps.eu10.hana.ondemand.com"
  }]
}
  • プロジェクト直下のcredentials.jsonファイルで接続先の認証情報を設定(※認証がない場合でもusername、passwordを設定する必要がある)
credentials.json
{
  "credentials": [{
    "alias": "MockServer",
    "username": "dummy",
    "password": "dummy"
  }]
}

上記の設定後、テストは正常終了します。

---------------------------------|---------|----------|---------|---------|-------------------
File                             | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
---------------------------------|---------|----------|---------|---------|-------------------
All files                        |   13.04 |        0 |   14.29 |   11.76 |                   
 src                             |       0 |        0 |       0 |       0 |                   
  app.controller.ts              |       0 |      100 |       0 |       0 | 1-10              
  app.module.ts                  |       0 |      100 |     100 |       0 | 1-12              
  app.service.ts                 |       0 |      100 |       0 |       0 | 1-6               
  main.ts                        |       0 |        0 |       0 |       0 | 1-8
 src/business-partner            |      30 |      100 |   33.33 |   28.57 | 
  business-partner.controller.ts |       0 |      100 |       0 |       0 | 1-13
  business-partner.module.ts     |       0 |      100 |     100 |       0 | 1-9
  business-partner.service.ts    |     100 |      100 |     100 |     100 | 
---------------------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        8.797 s
Ran all test suites matching /business-partner.service/i.

##e2e Test
testフォルダの下にapp.e.2-spec.tsというファイルがありますが、これをコピーしてbusiness-partner.e2e-spec.tsを作成します。
image.png

モジュール名をBusinessPartnerModuleに変更します。また、Destinationを利用するため、mockTestDestinationをインポートして使用します。

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { BusinessPartnerModule } from './../src/business-partner'; //変更
import { mockTestDestination } from '@sap-cloud-sdk/test-util'; //追加

describe('BusinessPartnerController (e2e)', () => { //変更
  let app: INestApplication;
  mockTestDestination('MockServer'); //追加

  beforeEach(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [BusinessPartnerModule], //変更
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  //ここにテストを書く
});

テストすることは、エンドポイント/business-partnerを呼んだときに想定するレスポンスが返ってくるかということです。そこで、Supertestを使って以下のようにテストを書きます。
※mockBusinessPartnersはServiceのテストで使用したものと同じ内容で定義しておきます。

  it('/business-partner (GET)', () => {
    return request(app.getHttpServer())
      .get('/business-partner')
      .expect(200)
      .expect(mockBusinessPartners);
  });

テストは以下のように正常終了します。

 PASS  test/business-partner.e2e-spec.ts (11.041 s)
  BusinessPartnerController (e2e)
    √ /business-partner (GET) (1931 ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files |       0 |        0 |       0 |       0 | 
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        12.126 s, estimated 16 s
Ran all test suites matching /business-partner/i.

##おわりに
今回はSAP Cloud SDKでテストを書く方法について、簡単な例でご紹介しました。POSTやPUT、DELETEのテストについては以下のリポジトリにテストを書いたものを格納していますので、参考にしてください。

##リファレンス

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?