SAP CAP上でDI/DIコンテナをサクッと実装: Node.js ハンズオン編

Estimated read time 10 min read

はじめに

理論編でDI/DIコンテナの概要について理解できたと思いますので、今回は実践に落とし込んでみたいと思います。ここでは、CAPやTypeScriptの使い方を知っており、VSCodeを使っている前提で進めます。

プロジェクトのフォルダー構成

多くのCAPユーザーに馴染みがあり、サクッと試せるハンズオンにするため、以下の特徴を取り入れました。

CAPの公式サンプルであるbookshopを参考にプロジェクトを作成。ディレクトリ構成をシンプルにするため、srvとtestディレクトリでサブディレクトリを作成しない。DI/DIコンテナにフォーカスするため、他のデザインパターンや不要なLayerは省略。コード量削減のためにany型などを一部使用。(実際の開発では非推奨。)外部DBは使用せず、csvファイルデータとモックデータを使用。レポジトリレイヤーのみをDIコンテナに登録。

それでは始めましょう!

CAPプロジェクトの作成

まずは、CAPのGetting Startedに倣ってプロジェクトを作成します。

npm add -g @sap/cds-dk

@SAP/cds-dkをインストールできたら、下記のコマンドを実行してください。

# cdsのインストールが正常に行えたかチェック
cds

# cdsプロジェクトを作成
cds init bookshop-di

# bookshopのディレクトリーのVSCodeを開く
code bookshop-di

Macユーザーは、下記を参考にcodeを有効化してください。

Assumes you activated the code command on macOS as documented

必要なパッケージのインストール

プロジェクトで使用するパッケージをインストールします。InversifyJSというライブラリを使って依存性の注入(DI)を行います。

# TypeScript
npm install -g typescript ts-node

# InversifyJS
npm install inversify reflect-metadata

# Jest
npm install –save-dev jest ts-jest @types/jest

必要なパッケージがインストールできましたので、各ディレクトリのコードを見ていきましょう。

ルートディレクトリ

InversifyJSを使うために、experimentalDecoratorsとemitDecoratorMetadataをtrueにした、tsconfig.jsonファイルを作成してください。

// tsconfig.json
{
“compilerOptions”: {
“target”: “es6”,
“module”: “commonjs”,
“strict”: true,
“esModuleInterop”: true,
“skipLibCheck”: false,
“forceConsistentCasingInFileNames”: true,
“experimentalDecorators”: true,
“emitDecoratorMetadata”: true
},
“include”: [“srv/**/*”, “test/**/*”],
“exclude”: [“node_modules”]
}

単体テストで使用するJestに関する設定ファイルです。

// jest.config.js
module.exports = {
preset: “ts-jest”,
testEnvironment: “node”,
};

dbディレクトリ

bookshopのエンティティとデータを参考に、簡素化したものを使用します。

cdsファイルのエンティティはこちらです。

// db/schema.cds
entity Books {
key ID : Integer;
title : localized String(111) @mandatory;
stock : Integer;
}

csvファイルのデータはこちらです。

# db/data/Books.csv
ID,title,stock
201,Wuthering Heights,101,
207,Jane Eyre,107

srvディレクトリ

Bookの型定義です。

// srv/book.ts
export interface Book {
ID: number;
title: string;
stock: number;
}

cdsファイルです。

// srv/catalogService.cds
using {Books as DbBooks} from ‘../db/schema’;

service CatalogService {
entity Books as projection on DbBooks;

action submitOrder(book : Integer, quantity : Integer) returns Books;
}

これからDIと関連する部分についてレポジトリ、ハンドラー、サービス、DIコンテナという順にコードを見ていきます。

まずはIBookRepositoryから解説していきます。submitOrderHandlerというビジネスロジックで使用するためのfindBookByIdとupdateBookStockを定義します。ビジネスロジックでは、findBookByIdで対象の本を取得し、注文の数量を満たす在庫があるかどうか確認し、在庫があればupdateBookStockで本の在庫量を更新するという使われ方になります。

// srv/iBookRepository.ts
import { Book } from “./book”;

export interface IBookRepository {
findBookById(ID: number): Promise<Book | undefined>;
updateBookStock(ID: number, newStock: number): Promise<void>;
}

次は具象クラスで、findBookByIdとupdateBookStockの実装内容を記載します。ここでは、Inversifyライブラリで提供されているinjectableデコレーターを使うことで、DIコンテナがクラスから発行されたメタデータを受け取るために使われます。

// srv/bookRepository.ts
import { IBookRepository } from “./iBookRepository”;
import { Book } from “./book”;
import { injectable } from “inversify”;

@injectable()
class BookRepository implements IBookRepository {
async findBookById(ID: number): Promise<Book | undefined> {
return await SELECT.from(“Books”, ID, (b: Book) => ({
ID: b.ID,
stock: b.stock,
title: b.title,
}));
}

async updateBookStock(ID: number, newStock: number): Promise<void> {
await UPDATE(“Books”, ID).with({ stock: newStock });
}
}

export { BookRepository };

次はsubmitOrderHandlerの解説です。

この関数内でBookRepositoryをインスタンス化せず、引数を使ってIBookRepositoryという抽象型に依存している点が肝になります。このおかげで、submitOrderHandlerはIBookRepositoryの型を持ったどのようなRepositoryも使用できるため、ローカル環境でテストをする際にはBookRepository、単体テストの際にはMockBookRepositoryを利用できるようになります。この疎結合がテスト容易性をもたらしています。

Note: 開発環境と本番環境のデータ基盤の切り替えは、DIコンテナを使わずとも、CAPが提供している機能を使ってpackage.jsonで切り替えられます

// srv/submitOrderHandler.ts
import { Request } from “@sap/cds”;
import { IBookRepository } from “./iBookRepository”;

export const submitOrderHandler =
(bookRepository: IBookRepository) =>
// ハンズオン用にコードをシンプルにするため、リクエストの型定義はせずにcdsのRequest型を使用。
async (req: Request) => {
const { book: ID, quantity } = req.data;

const book = await bookRepository.findBookById(ID);
if (!book) {
return req.error(404, `Book #${ID} doesn’t exist`);
}

if (quantity < 1) {
return req.error(400, `quantity has to be 1 or more`);
}
if (quantity > book.stock) {
return req.error(409, `${quantity} exceeds stock for book #${ID}`);
}
// ハンズオン用にコードをシンプルにするため、setterは省略。
book.stock -= quantity;
await bookRepository.updateBookStock(ID, book.stock);
return book;
};

CatalogServiceでbookRepositoryを定義し、submitOrderHandlerの引数として渡します。CAPの仕様上、catalogService.ts(とcatalogService.cds)の情報をもとにAPIを公開するようなRouteレイヤーの役割を果たすため、このサービスクラスを注入する必要がなく、併せてinjectableデコレーターもいらないと考えられます。

// srv/catalogService.ts
import { IBookRepository } from “./iBookRepository”;
import { container } from “./inversify.config”;
import { submitOrderHandler } from “./submitOrderHandler”;
import { TYPES } from “./types”;
import cds from “@sap/cds”;

class CatalogService extends cds.ApplicationService {
async init() {
const bookRepository = container.get<IBookRepository>(
TYPES.IBookRepository
);

this.on(“submitOrder”, submitOrderHandler(bookRepository));
}
}

export default CatalogService;

最後に、DIコンテナを設定します。submitOrderHandlerやCatalogServiceにBookRepositoryといった具象クラスが存在しないため、どのように呼び出すのか疑問に思われたかもしれませんが、その点についてはDIコンテナが解決します。まさに、読んで字の如く「DIコンテナ」です。

実際のコードが以下の2つのファイルです。まずはシンボル(一意の識別子)を定義します。

// srv/types.ts
export const TYPES = {
IBookRepository: Symbol.for(“IBookRepository”),
};

DIコンテナでTYPES.IBookRepositoryという識別子とBookRepository クラスをバインドします。これにより、コンテナのTYPES.IBookRepositoryが要求された際にBookRepositoryという具象クラスを呼び出しています。

// srv/inversify.config.ts
import { Container } from “inversify”;
import { IBookRepository } from “./iBookRepository”;
import { BookRepository } from “./bookRepository”;
import { TYPES } from “./types”;

export const container = new Container();
container.bind<IBookRepository>(TYPES.IBookRepository).to(BookRepository);

補足: シンボルを使わなくてもDIコンテナを利用できます。具体的には、bindの引数であるTYPES.IBookRepositoryをBookRepositoryというクラスに書き換える、あるいは’IBookRepository’という文字列に書き換える形でも利用可能です。(興味のある方は、試してみてください。) しかしながら、前者はcontainerを呼び出すCatalogServiceがBookRepositoryの詳細を知ってしまうため、後者は一意性の保証ができないため、推奨されません。

By using symbols, you can provide interface implementations in a way that the dependent class is not aware of the dependency implementation details.

 

以上でAPIをコールするための準備は整いましたので、下記コマンドでローカルホストを立ち上げてテストします。

cds-ts w

上記で4004ポートのサーバーを立ち上げた状態で、httpファイルで定義したPOSTリクエストを送ります。「Send Request」というテキストがhttpファイルに表示されない場合には、「Rest Client」という拡張機能をインストールしてください。

注意点: BookRepositoryでUPDATE処理を書いていますが、cdsファイルで定義したactionはPOST処理とみなされます。

// test/requests.http
###
POST http://localhost:4004/odata/v4/catalog/submitOrder
Content-Type: application/json

{ “book”:201, “quantity”:5 }

200 OKが返ってきたら成功ですので、次のパートに進みましょう。エラーが発生した際には、各ファイル名が正しいか、保存されているか確認してください。

testディレクトリ

次は単体テストパートで、DIのメリットを享受できます。

まず、srv/inversify.config.tsと同じ形式でtestContainerを定義します。ただし、今回はMockBookRepositoryにバインドしている点に注意してください。

// test/inversify.test.config.ts
import { Container } from “inversify”;
import { IBookRepository } from “../srv/iBookRepository”;
import { TYPES } from “../srv/types”;
import { MockBookRepository } from “./mockBookRepository”;

export const testContainer = new Container();
testContainer
.bind<IBookRepository>(TYPES.IBookRepository)
.to(MockBookRepository);

MockBookRepositoryでは、下記のようなbookのモックデータを使用しています。

// test/mockBookRepository.ts
import { injectable } from “inversify”;
import { IBookRepository } from “../srv/iBookRepository”;
import { Book } from “../srv/book”;

@injectable()
class MockBookRepository implements IBookRepository {
private books: Book[] = [
{ ID: 1, title: “Test Book 1”, stock: 10 },
{ ID: 2, title: “Test Book 2”, stock: 5 },
];

async findBookById(ID: number): Promise<Book | undefined> {
return this.books.find((book) => book.ID === ID);
}

async updateBookStock(ID: number, newStock: number): Promise<void> {
const book = this.books.find((book) => book.ID === ID);
if (book) {
book.stock = newStock;
}
}
}

export { MockBookRepository };

最後に単体テストを書きますが、testContainerを使うことでsrvディレクトリで定義したBookRepositoryに依存せず、testディレクトリのMockBookRepositoryを利用している点を意識してみてください。

// test/submitOrderHandler.test.ts
import { submitOrderHandler } from “../srv/submitOrderHandler”;
import { IBookRepository } from “../srv/iBookRepository”;
import { testContainer } from “./inversify.test.config”;
import { TYPES } from “../srv/types”;
import { Book } from “../srv/book”;

describe(“submitOrderHandler”, () => {
let mockBookRepository: IBookRepository;
// ハンズオン用にコードをシンプルにするため、リクエストの型定義はせずにany型を使用。
let handler: (req: any) => Promise<Book | Error>;

beforeEach(() => {
mockBookRepository = testContainer.get<IBookRepository>(
TYPES.IBookRepository
);
handler = submitOrderHandler(mockBookRepository);
});

it(“should return the book and update stock when order is successful”, async () => {
const req = {
data: { book: 1, quantity: 2 },
error: jest.fn(),
};

const result = await handler(req);

expect(result).toEqual({ ID: 1, title: “Test Book 1”, stock: 8 });
expect(mockBookRepository.findBookById(1)).resolves.toEqual({
ID: 1,
title: “Test Book 1”,
stock: 8,
});
expect(req.error).not.toHaveBeenCalled();
});

it(“should return 404 error if book does not exist”, async () => {
const req = {
data: { book: 999, quantity: 2 },
error: jest.fn(),
};

await handler(req);

expect(req.error).toHaveBeenCalledWith(404, “Book #999 doesn’t exist”);
});

it(“should return 400 error if quantity is less than 1”, async () => {
const req = {
data: { book: 1, quantity: 0 },
error: jest.fn(),
};

await handler(req);

expect(req.error).toHaveBeenCalledWith(400, “quantity has to be 1 or more”);
});

it(“should return 409 error if quantity exceeds stock”, async () => {
const req = {
data: { book: 2, quantity: 10 },
error: jest.fn(),
};

await handler(req);

expect(req.error).toHaveBeenCalledWith(409, “10 exceeds stock for book #2”);
});
});

最後に、ターミナルにてjestコマンドを実行してテストが成功すれば、ハンズオン終了です。

npx jest

まとめ

今回のハンズオンではSAP CAP上でDI/DIコンテナを活用し、TypeScriptを用いたプロジェクトを構築しました。結果として、レポジトリレイヤーとハンドラレイヤーが疎結合になり、テスト容易性が向上しました。

次のステップとしては、bookshopのAuthorsなどのEntityの追加、DDDやClean Architectureへの拡張、カスタムロジックの追加などを試してみてください。

参考リンク

CAP – Getting StartedCAP – Using TypeScriptGitHub – cloud-sap-samples > bookshop 

​ はじめに理論編でDI/DIコンテナの概要について理解できたと思いますので、今回は実践に落とし込んでみたいと思います。ここでは、CAPやTypeScriptの使い方を知っており、VSCodeを使っている前提で進めます。プロジェクトのフォルダー構成多くのCAPユーザーに馴染みがあり、サクッと試せるハンズオンにするため、以下の特徴を取り入れました。CAPの公式サンプルであるbookshopを参考にプロジェクトを作成。ディレクトリ構成をシンプルにするため、srvとtestディレクトリでサブディレクトリを作成しない。DI/DIコンテナにフォーカスするため、他のデザインパターンや不要なLayerは省略。コード量削減のためにany型などを一部使用。(実際の開発では非推奨。)外部DBは使用せず、csvファイルデータとモックデータを使用。レポジトリレイヤーのみをDIコンテナに登録。それでは始めましょう!CAPプロジェクトの作成まずは、CAPのGetting Startedに倣ってプロジェクトを作成します。npm add -g @sap/cds-dk@SAP/cds-dkをインストールできたら、下記のコマンドを実行してください。# cdsのインストールが正常に行えたかチェック
cds

# cdsプロジェクトを作成
cds init bookshop-di

# bookshopのディレクトリーのVSCodeを開く
code bookshop-diMacユーザーは、下記を参考にcodeを有効化してください。Assumes you activated the code command on macOS as documented必要なパッケージのインストールプロジェクトで使用するパッケージをインストールします。InversifyJSというライブラリを使って依存性の注入(DI)を行います。# TypeScript
npm install -g typescript ts-node

# InversifyJS
npm install inversify reflect-metadata

# Jest
npm install –save-dev jest ts-jest @types/jest必要なパッケージがインストールできましたので、各ディレクトリのコードを見ていきましょう。ルートディレクトリInversifyJSを使うために、experimentalDecoratorsとemitDecoratorMetadataをtrueにした、tsconfig.jsonファイルを作成してください。// tsconfig.json
{
“compilerOptions”: {
“target”: “es6”,
“module”: “commonjs”,
“strict”: true,
“esModuleInterop”: true,
“skipLibCheck”: false,
“forceConsistentCasingInFileNames”: true,
“experimentalDecorators”: true,
“emitDecoratorMetadata”: true
},
“include”: [“srv/**/*”, “test/**/*”],
“exclude”: [“node_modules”]
}単体テストで使用するJestに関する設定ファイルです。// jest.config.js
module.exports = {
preset: “ts-jest”,
testEnvironment: “node”,
};dbディレクトリbookshopのエンティティとデータを参考に、簡素化したものを使用します。cdsファイルのエンティティはこちらです。// db/schema.cds
entity Books {
key ID : Integer;
title : localized String(111) @mandatory;
stock : Integer;
}csvファイルのデータはこちらです。# db/data/Books.csv
ID,title,stock
201,Wuthering Heights,101,
207,Jane Eyre,107srvディレクトリBookの型定義です。// srv/book.ts
export interface Book {
ID: number;
title: string;
stock: number;
}cdsファイルです。// srv/catalogService.cds
using {Books as DbBooks} from ‘../db/schema’;

service CatalogService {
entity Books as projection on DbBooks;

action submitOrder(book : Integer, quantity : Integer) returns Books;
}これからDIと関連する部分についてレポジトリ、ハンドラー、サービス、DIコンテナという順にコードを見ていきます。まずはIBookRepositoryから解説していきます。submitOrderHandlerというビジネスロジックで使用するためのfindBookByIdとupdateBookStockを定義します。ビジネスロジックでは、findBookByIdで対象の本を取得し、注文の数量を満たす在庫があるかどうか確認し、在庫があればupdateBookStockで本の在庫量を更新するという使われ方になります。// srv/iBookRepository.ts
import { Book } from “./book”;

export interface IBookRepository {
findBookById(ID: number): Promise<Book | undefined>;
updateBookStock(ID: number, newStock: number): Promise<void>;
}次は具象クラスで、findBookByIdとupdateBookStockの実装内容を記載します。ここでは、Inversifyライブラリで提供されているinjectableデコレーターを使うことで、DIコンテナがクラスから発行されたメタデータを受け取るために使われます。// srv/bookRepository.ts
import { IBookRepository } from “./iBookRepository”;
import { Book } from “./book”;
import { injectable } from “inversify”;

@injectable()
class BookRepository implements IBookRepository {
async findBookById(ID: number): Promise<Book | undefined> {
return await SELECT.from(“Books”, ID, (b: Book) => ({
ID: b.ID,
stock: b.stock,
title: b.title,
}));
}

async updateBookStock(ID: number, newStock: number): Promise<void> {
await UPDATE(“Books”, ID).with({ stock: newStock });
}
}

export { BookRepository };次はsubmitOrderHandlerの解説です。この関数内でBookRepositoryをインスタンス化せず、引数を使ってIBookRepositoryという抽象型に依存している点が肝になります。このおかげで、submitOrderHandlerはIBookRepositoryの型を持ったどのようなRepositoryも使用できるため、ローカル環境でテストをする際にはBookRepository、単体テストの際にはMockBookRepositoryを利用できるようになります。この疎結合がテスト容易性をもたらしています。Note: 開発環境と本番環境のデータ基盤の切り替えは、DIコンテナを使わずとも、CAPが提供している機能を使ってpackage.jsonで切り替えられます。// srv/submitOrderHandler.ts
import { Request } from “@sap/cds”;
import { IBookRepository } from “./iBookRepository”;

export const submitOrderHandler =
(bookRepository: IBookRepository) =>
// ハンズオン用にコードをシンプルにするため、リクエストの型定義はせずにcdsのRequest型を使用。
async (req: Request) => {
const { book: ID, quantity } = req.data;

const book = await bookRepository.findBookById(ID);
if (!book) {
return req.error(404, `Book #${ID} doesn’t exist`);
}

if (quantity < 1) {
return req.error(400, `quantity has to be 1 or more`);
}
if (quantity > book.stock) {
return req.error(409, `${quantity} exceeds stock for book #${ID}`);
}
// ハンズオン用にコードをシンプルにするため、setterは省略。
book.stock -= quantity;
await bookRepository.updateBookStock(ID, book.stock);
return book;
};CatalogServiceでbookRepositoryを定義し、submitOrderHandlerの引数として渡します。CAPの仕様上、catalogService.ts(とcatalogService.cds)の情報をもとにAPIを公開するようなRouteレイヤーの役割を果たすため、このサービスクラスを注入する必要がなく、併せてinjectableデコレーターもいらないと考えられます。// srv/catalogService.ts
import { IBookRepository } from “./iBookRepository”;
import { container } from “./inversify.config”;
import { submitOrderHandler } from “./submitOrderHandler”;
import { TYPES } from “./types”;
import cds from “@sap/cds”;

class CatalogService extends cds.ApplicationService {
async init() {
const bookRepository = container.get<IBookRepository>(
TYPES.IBookRepository
);

this.on(“submitOrder”, submitOrderHandler(bookRepository));
}
}

export default CatalogService;最後に、DIコンテナを設定します。submitOrderHandlerやCatalogServiceにBookRepositoryといった具象クラスが存在しないため、どのように呼び出すのか疑問に思われたかもしれませんが、その点についてはDIコンテナが解決します。まさに、読んで字の如く「DIコンテナ」です。実際のコードが以下の2つのファイルです。まずはシンボル(一意の識別子)を定義します。// srv/types.ts
export const TYPES = {
IBookRepository: Symbol.for(“IBookRepository”),
};DIコンテナでTYPES.IBookRepositoryという識別子とBookRepository クラスをバインドします。これにより、コンテナのTYPES.IBookRepositoryが要求された際にBookRepositoryという具象クラスを呼び出しています。// srv/inversify.config.ts
import { Container } from “inversify”;
import { IBookRepository } from “./iBookRepository”;
import { BookRepository } from “./bookRepository”;
import { TYPES } from “./types”;

export const container = new Container();
container.bind<IBookRepository>(TYPES.IBookRepository).to(BookRepository);補足: シンボルを使わなくてもDIコンテナを利用できます。具体的には、bindの引数であるTYPES.IBookRepositoryをBookRepositoryというクラスに書き換える、あるいは’IBookRepository’という文字列に書き換える形でも利用可能です。(興味のある方は、試してみてください。) しかしながら、前者はcontainerを呼び出すCatalogServiceがBookRepositoryの詳細を知ってしまうため、後者は一意性の保証ができないため、推奨されません。’By using symbols, you can provide interface implementations in a way that the dependent class is not aware of the dependency implementation details.’ 以上でAPIをコールするための準備は整いましたので、下記コマンドでローカルホストを立ち上げてテストします。cds-ts w上記で4004ポートのサーバーを立ち上げた状態で、httpファイルで定義したPOSTリクエストを送ります。「Send Request」というテキストがhttpファイルに表示されない場合には、「Rest Client」という拡張機能をインストールしてください。注意点: BookRepositoryでUPDATE処理を書いていますが、cdsファイルで定義したactionはPOST処理とみなされます。// test/requests.http
###
POST http://localhost:4004/odata/v4/catalog/submitOrder
Content-Type: application/json

{ “book”:201, “quantity”:5 }200 OKが返ってきたら成功ですので、次のパートに進みましょう。エラーが発生した際には、各ファイル名が正しいか、保存されているか確認してください。testディレクトリ次は単体テストパートで、DIのメリットを享受できます。まず、srv/inversify.config.tsと同じ形式でtestContainerを定義します。ただし、今回はMockBookRepositoryにバインドしている点に注意してください。// test/inversify.test.config.ts
import { Container } from “inversify”;
import { IBookRepository } from “../srv/iBookRepository”;
import { TYPES } from “../srv/types”;
import { MockBookRepository } from “./mockBookRepository”;

export const testContainer = new Container();
testContainer
.bind<IBookRepository>(TYPES.IBookRepository)
.to(MockBookRepository);MockBookRepositoryでは、下記のようなbookのモックデータを使用しています。// test/mockBookRepository.ts
import { injectable } from “inversify”;
import { IBookRepository } from “../srv/iBookRepository”;
import { Book } from “../srv/book”;

@injectable()
class MockBookRepository implements IBookRepository {
private books: Book[] = [
{ ID: 1, title: “Test Book 1”, stock: 10 },
{ ID: 2, title: “Test Book 2”, stock: 5 },
];

async findBookById(ID: number): Promise<Book | undefined> {
return this.books.find((book) => book.ID === ID);
}

async updateBookStock(ID: number, newStock: number): Promise<void> {
const book = this.books.find((book) => book.ID === ID);
if (book) {
book.stock = newStock;
}
}
}

export { MockBookRepository };最後に単体テストを書きますが、testContainerを使うことでsrvディレクトリで定義したBookRepositoryに依存せず、testディレクトリのMockBookRepositoryを利用している点を意識してみてください。// test/submitOrderHandler.test.ts
import { submitOrderHandler } from “../srv/submitOrderHandler”;
import { IBookRepository } from “../srv/iBookRepository”;
import { testContainer } from “./inversify.test.config”;
import { TYPES } from “../srv/types”;
import { Book } from “../srv/book”;

describe(“submitOrderHandler”, () => {
let mockBookRepository: IBookRepository;
// ハンズオン用にコードをシンプルにするため、リクエストの型定義はせずにany型を使用。
let handler: (req: any) => Promise<Book | Error>;

beforeEach(() => {
mockBookRepository = testContainer.get<IBookRepository>(
TYPES.IBookRepository
);
handler = submitOrderHandler(mockBookRepository);
});

it(“should return the book and update stock when order is successful”, async () => {
const req = {
data: { book: 1, quantity: 2 },
error: jest.fn(),
};

const result = await handler(req);

expect(result).toEqual({ ID: 1, title: “Test Book 1”, stock: 8 });
expect(mockBookRepository.findBookById(1)).resolves.toEqual({
ID: 1,
title: “Test Book 1”,
stock: 8,
});
expect(req.error).not.toHaveBeenCalled();
});

it(“should return 404 error if book does not exist”, async () => {
const req = {
data: { book: 999, quantity: 2 },
error: jest.fn(),
};

await handler(req);

expect(req.error).toHaveBeenCalledWith(404, “Book #999 doesn’t exist”);
});

it(“should return 400 error if quantity is less than 1”, async () => {
const req = {
data: { book: 1, quantity: 0 },
error: jest.fn(),
};

await handler(req);

expect(req.error).toHaveBeenCalledWith(400, “quantity has to be 1 or more”);
});

it(“should return 409 error if quantity exceeds stock”, async () => {
const req = {
data: { book: 2, quantity: 10 },
error: jest.fn(),
};

await handler(req);

expect(req.error).toHaveBeenCalledWith(409, “10 exceeds stock for book #2”);
});
});最後に、ターミナルにてjestコマンドを実行してテストが成功すれば、ハンズオン終了です。npx jestまとめ今回のハンズオンではSAP CAP上でDI/DIコンテナを活用し、TypeScriptを用いたプロジェクトを構築しました。結果として、レポジトリレイヤーとハンドラレイヤーが疎結合になり、テスト容易性が向上しました。次のステップとしては、bookshopのAuthorsなどのEntityの追加、DDDやClean Architectureへの拡張、カスタムロジックの追加などを試してみてください。参考リンクCAP – Getting StartedCAP – Using TypeScriptGitHub – cloud-sap-samples > bookshop   Read More Technology Blogs by SAP articles 

#SAP

#SAPTechnologyblog

You May Also Like

More From Author