##はじめに
この記事は、SAP Cloud SDK for JavaScriptを使ってみるシリーズの3回目です。
SAP Cloud SDK for JavaScriptのプロジェクトはNestJSをベースに作られており、NestJSはJestとSupsertestを使用したテストの仕組みを備えています。この仕組みを利用した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用です。
###スクリプト
package.jsonにはテスト用のスクリプトが定義されています。今回、Unit Testではtest
またはtest:watch
、e2e Testではtest:e2e
を使用します。
test:watch
は、テスト対象のファイルが変更されると自動的にテストを再実行してくれます。
"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のパイプラインの中で実行するとテスト結果を見やすい形にして表示してくれます。これについては、次の回で紹介したいと思います。
以降では、実際にUnit Test, Integration Testをどのように書くかを見ていきます。
##Unit Test
###Controllerのテスト
business-partner.controller.ts
の処理をbusiness-partner.controller.spec.ts
でテストします。
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();
}
mockBusinessPartnerService
にgetAllBusinessPartners
メソッドを追加します。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つのメソッドを使ってテストをしています。
-
expect(value1).toEqual(value2)
: value1とvalue2が同じプロパティを持っているかどうか確認するために使います。 -
expect(value).toHaveBeenCalled()
: モックのファンクションが呼ばれたことを確認するために使います。
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
でテストします。
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を設定- URLにはCloud Foudnryに登録したBusiness Partner ODataサービスの宛先を指定します。
{
"systems": [{
"alias": "MockServer",
"uri": "https://odata-mock-server-forgiving-hartebeest.cfapps.eu10.hana.ondemand.com"
}]
}
- プロジェクト直下の
credentials.json
ファイルで接続先の認証情報を設定(※認証がない場合でもusername、passwordを設定する必要がある)
{
"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
を作成します。
モジュール名を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のテストについては以下のリポジトリにテストを書いたものを格納していますので、参考にしてください。
##リファレンス