はじめに
理論編で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の詳細を知ってしまうため、後者は一意性の保証ができないため、推奨されません。
以上で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