【Day 16】ブログ一覧を取得し、表示する - BFF Module

## はじめに

![スライド17.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985550/7e5ba0db-3902-b78c-df9c-100bc9cee9b3.png)


---

**2023年アドベントカレンダー16日目**です。

https://qiita.com/advent-calendar/2023/life-sync-app

現在「ユーザーはトップページでブログの一覧を見ることができる」を進めています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985550/272b7c0a-ca22-3db9-acc8-f52a2dd4a280.png)

前回はGraphQLModuleの設定を行いました。
今回はメインの実装を進めていきます。

## E2Eテスト

`nest new`でNestJSを作成したときにすでにE2Eテストの概形はできているので、テストを追加していきます。

:::note warn
もっとGraphQLサーバーに特化した書き方があるかもしれません
:::

POSTでqueryを受け取り、期待するレスポンスを返せるかを確認します。

```ts:app.e2e-spec.ts
  it('/ (Query) GET BLOGS', async () => {
    const result = await request(app.getHttpServer()).post('/').send({
      query: '{ blogs { id title author body createdAt } }',
    });

    expect(result.status).toEqual(200);
    expect(result.body.data.blogs).toEqual([
      {
        id: '8c7cf457-5ffe-44c3-98b3-c529c09e9999',
        title: 'title1',
        author: 'author1',
        body: 'body1',
        createdAt: '2019-01-01T00:00:00.000Z',
      },
    ]);
  });
```

```bash
bff$ npm run test:e2e

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
```

:ok: 実装前なので、**期待通り<font color="red">失敗</font>** しました

## 実装

今回はこのような構成を目指しています。

```plantuml
@startuml

package App <<Rectangle>> {
  class AppModule {
    +GraphQLModule
  }
}

package GraphQL <<Rectangle>> {
  class GraphQLModule {}
}

package Blog <<Rectangle>> {
  class BlogModule {
  }

  class BlogResolver {
    +blogService: BlogService
    +get(): Promise<Blog[]>
  }
  class BlogService {
    +get(): Promise<BlogJson[]>
  }
}

AppModule -right-> GraphQLModule : imports
AppModule  --> BlogModule : imports
BlogModule --> BlogResolver : providers
BlogModule --> BlogService : providers
BlogResolver -right-> BlogService : uses
@enduml
```

その中のこの部分を作っていきます！

```plantuml
package App <<Rectangle>> {
  class AppModule {
    +GraphQLModule
  }
}

package Blog <<Rectangle>> {
  class BlogModule {
  }

  class BlogResolver {
    +blogService: BlogService
    +get(): Promise<Blog[]>
  }
  class BlogService {
    +get(): Promise<BlogJson[]>
  }
}

AppModule  --> BlogModule : imports
BlogModule --> BlogResolver : providers
BlogModule --> BlogService : providers
BlogResolver -right-> BlogService : uses
@enduml
```

### BlogModule

```plantuml
package App <<Rectangle>> {
  class AppModule {
    +GraphQLModule
  }
}

package Blog <<Rectangle>> {
  class BlogModule {
  }
}

AppModule  --> BlogModule : imports
@enduml
```

CLIで作成していきます

```bash
$ nest g module blog
CREATE src/blog/blog.module.ts (88 bytes)
UPDATE src/app.module.ts (700 bytes)
```

AppModuleへのインポートまでやってくれます
Moduleでやりたいことはもう無いので次に行きます

---

:::note warn
この先は単体テストを書いていくので、下の図のような流れで実装していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985550/e718faa1-3ddd-a7e2-da7d-f2a402d4e2f8.png)
:::

### BlogService

BlogServiceを作っていきます。

```plantuml
package Blog <<Rectangle>> {
  class BlogService {
    +get(): Promise<BlogJson[]>
  }
}
@enduml
```

:::note info
#### 責務
このServiceでは、APIからデータを取得して、BlogJsonに変換し、Resolverに返すことを責務としました
:::

```bash
$ nest g service blog
CREATE src/blog/blog.service.spec.ts (446 bytes)
CREATE src/blog/blog.service.ts (88 bytes)
UPDATE src/blog/blog.module.ts (155 bytes)
```

単体テスト用のSpecファイルと、BlogModuleへのインポートまでやってくれます(**便利!!**) 👍

#### テスト

:::note info
工夫したポイント
axiosのgetメソッドをモックして、思った結果が返ってくるかを確認しました
:::

<details><summary>blog.service.spec.ts</summary>

```ts
  it('should return blogs json', async () => {
    const originalAxiosGet = axios.get;
    const response = {
      data: {
        blogs: [
          {
            id: 'id 1',
            title: 'Blog 1',
            author: 'Author 1',
            body: 'Body 1',
            createdAt: '2023-12-01 09:00:00',
          },
          {
            id: 'id 2',
            title: 'Blog 2',
            author: 'Author 2',
            body: 'Body 2',
            createdAt: '2023-12-02 09:00:00',
          },
        ],
      },
    };

    axios.get = jest.fn().mockResolvedValue(response);
    const actual = await service.get();
    const expected = new BlogsJson(response.data);

    expect(actual).toEqual(expected);

    axios.get = originalAxiosGet;
  });
```

</details>

```bash
 FAIL  src/blog/blog.service.spec.ts

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        1.02 s, estimated 2 s
```

:ok: 実装前なので、**期待通り<font color="red">失敗</font>** しました

#### 実装

<details><summary>blog.service.ts</summary>

```ts
import { Injectable } from '@nestjs/common';
import axios from 'axios';

export class BlogJson {
  constructor(
    public id: string,
    public title: string,
    public author: string,
    public body: string,
    public createdAt: string,
  ) {}
}

export class BlogsJson {
  constructor(data: any) {
    this.blogs = data.blogs.map(
      (d: any) => new BlogJson(d.id, d.title, d.author, d.body, d.createdAt),
    );
  }
  blogs: BlogJson[];
}

@Injectable()
export class BlogService {
  async get(): Promise<BlogsJson> {
    const response = await axios.get('http://localhost:8080/blogs');
    return new BlogsJson(response.data);
  }
}
```
</details>

```bash
 PASS  src/blog/blog.service.spec.ts
  BlogResolver
    ✓ should be defined (4 ms)
    ✓ should return blogs json (1 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.02 s, estimated 2 s
```

実装したので通りました👍

### BlogResolver

最後にBlogResolverを作っていきます。

```plantuml
package Blog <<Rectangle>> {
  class BlogResolver {
    +blogService: BlogService
    +get(): Promise<Blog[]>
  }
}
@enduml
```

:::note info

#### 責務
このResolverでは、BlogServiceから受け取ったBlogJsonオブジェクトをSchemaファイルに合わせたBlogオブジェクトに変換して返す
:::

```bash
$ nest g resolver blog
CREATE src/blog/blog.resolver.spec.ts (456 bytes)
CREATE src/blog/blog.resolver.ts (86 bytes)
UPDATE src/blog/blog.module.ts (217 bytes)
```

単体テスト用のSpecファイルと、BlogModuleへのインポートまでやってくれます(**便利!!**) 👍

#### テスト

:::note info
工夫したポイント
serviceのgetをモックしました
:::

<details><summary>blog.resolver.spec.ts</summary>

```ts:blog.resolver.spec.ts
  it('should return blogs', async () => {
    const mockBlogService = {} as BlogService;
    mockBlogService.get = jest.fn().mockResolvedValue(
      new BlogsJson({
        blogs: [
          {
            id: 'id 1',
            title: 'Blog 1',
            author: 'Author 1',
            body: 'Body 1',
            createdAt: '2023-12-01 09:00:00',
          },
          {
            id: 'id 2',
            title: 'Blog 2',
            author: 'Author 2',
            body: 'Body 2',
            createdAt: '2023-12-02 09:00:00',
          },
        ],
      }),
    );

    const expected = [
      new Blog('id 1', 'Blog 1', 'Author 1', 'Body 1', '2023-12-01 09:00:00'),
      new Blog('id 2', 'Blog 2', 'Author 2', 'Body 2', '2023-12-02 09:00:00'),
    ];

    const resolver = new BlogResolver(mockBlogService);
    const actual = await resolver.get();

    expect(actual).toEqual(expected);
  });
```

</details>

```bash
 FAIL  src/blog/blog.resolver.spec.ts

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        1.02 s, estimated 2 s
```

:ok: 実装前なので、**期待通り<font color="red">失敗</font>** しました

#### 実装

:::note info
工夫したポイント
自動生成されるSchemaの型ファイルをimplementした、Blogクラスを作成し、それを返却するようにしました。
:::

<details><summary>blog.resolver.ts</summary>

```ts:blog.resolver.ts
import { Query, Resolver } from '@nestjs/graphql';
import { BlogService } from './blog.service';
import { Blog as IBlog } from '@/graphql/generated';

export class Blog implements IBlog {
  constructor(
    public id: string,
    public title: string,
    public author: string,
    public body: string,
    public createdAt: string,
  ) {}
}

@Resolver('Blog')
export class BlogResolver {
  constructor(private blogService: BlogService) {}

  @Query('blogs')
  async get(): Promise<Blog[]> {
    const blogsJson = await this.blogService.get();
    return blogsJson.blogs.map(
      (blogJson) =>
        new Blog(
          blogJson.id,
          blogJson.title,
          blogJson.author,
          blogJson.body,
          blogJson.createdAt,
        ),
    );
  }
}
```

</details>

```bash
 PASS  src/blog/blog.resolver.spec.ts
  BlogResolver
    ✓ should be defined (4 ms)
    ✓ should return blogs (1 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.02 s, estimated 2 s
```

実装したので通りました👍

## E2E

最後に環境を立ち上げてE2Eを回してみます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985550/985f26bc-a2d1-ce28-3015-3f8319ce5333.png)

```bash
 PASS  test/app.e2e-spec.ts
  AppController (e2e)
    ✓ / (Query) GET BLOGS (327 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.466 s
```

通りました👍

これでBFFの実装は終了なので、APIの実装に移ります。
次回はまずRustでのAPI開発の準備を進めていきます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985550/31801d92-3fcc-04fd-0b80-a797b8f34896.png)