NestJS TDD - UnitTest


バックエンド前処理コースの開発要件TDD(テスト駆動開発).
第1週目に第2の提示語課題を行う際にユニットテスト部を担当した
TDDとは、原理よりもTDDへのアクセスと使用が重要です.

イニシアチブ

  • ユーザを格納するために情報を受信する.
  • の新しいAPIを作成する必要があります.
  • 開発者本人を除き、外部でAPIが使用される.
  • Controller Unit Test


    テスト意図

  • 正しい入力
  • API SPEC

    {
       "id": 1
       "email": "wanted",
       "password": "wecode"
       "roles": "admin"
    }
    nest g module users
    nest g controller users
    npm test
    上に追加したコントローラから、2つのファイルに関するテストが成功したことがわかります.

    テストプロセス


    コントローラ->サービス->リポジトリ順に上から下へテストします.
    API SPECは明確なので、トップダウンの方が分かりやすいかもしれません.
    また,TDD毎に出現する図中の手順に従い,「失敗->再包装による」の順に操作する.
    @Controller('users')
    export class UsersController {}
    
    
    //test
    it('should call the service', () => {
        controller.create();
        
    });

    fail


    コントローラにはcreate機能がなく、サービスに階層が存在しません.
    IDEは、テストの作成中に何が欠けているかを警告しますが、テストを続行します.
    これは簡単なプロセスで、何が欠けているかを直感的に見ることができますが、作成する必要があるかを理解することができます.

    これで、コントローラの機能とサービス層を追加してテストに合格します.
    nest g s users

    pass?

    // controller
    @Controller('users')
    export class UsersController {
      create(createUserDto: any) {}
    }
    // service
    @Injectable()
    export class UsersService {
      create(createUserDto: any) {
        throw new Error('호출 될까요?');
      }
    }
    // test
    it('should call the service', () => {
        const createUserDto = {};
        controller.create(createUserDto);
        expect(service.create).toHaveBeenCalled();
    });
    上記のテストコードを作成する過程で、IDEはアラーム通知サービス方法によって呼び出される.
    もちろん、サービス・レイヤ・コールでサービスが見つからないため、テストは失敗します.

    依存性の問題をサービス層に注入することによって解決しなければならないことを見出した.
    次のように変更します.
    // controller
    @Controller('users')
    export class UsersController {
      create(createUserDto: any) {
        this.usersService.create(createUserDto);
      }
    }
    // service
    @Injectable()
    export class UsersService {
      create(createUserDto: any) {
        return 'pass?';
      }
    }
    // test
    it('should call the service', () => {
        const createUserDto = {};
        controller.create(createUserDto);
        expect(service.create).toHaveBeenCalled();
    });
    今、サービス層に依存性を設定しました.関数呼び出し時にIDEにも赤い線が見えますが!!
    テストは必ず合格すると信じています.

    でも。


    きれいにするために、テストはまた失敗した.テストの進行中に、入力されたオブジェクトはmockまたはspy関数でなければなりません.

    ほとんどの人がここで悠太を遊んでいますか?インポートを開始します.簡単なCRUDについては、上述した簡単な複雑な手順を実行しなくても、CRUDを直感的に実行することができる.テストプロセスが非常に複雑なため、直接作成して処理するのが非常に効率的ではないかと心配しています.間違いない.簡単なCRUDでは、テストを行う必要はありませんが、私たちの作業環境では、複雑な関係を処理するために多くの例外を準備する必要があります.
    コントローラ->サービスのメソッド呼び出しを表示するには、2つの方法でMock Serviceを作成します.
    各モジュールには、ベンダーとして異なる方法があることに注意してください.
    // jest 함수를 사용하여 만들 때
    jest.mock('./users.service');
    
    const module: TestingModule = await Test.createTestingModule({
          controllers: [UsersController],
          providers: [UsersService],
        }).compile();
    
    // MockService를 arrow func로 만들었을 때
    const mockService = () => ({
      create: jest.fn(),
    });
    
    const module: TestingModule = await Test.createTestingModule({
          controllers: [UsersController],
          providers: [
            {
              provide: UsersService,
              useValue: mockService(),
            },
          ],
        }).compile();
    では、上の方法にはいったいどんな違いがあるのでしょうか.

    jest.mock()


    各モジュールをより簡単にシミュレーションできるように、自動シミュレーションモジュールを冗談にしました.mock()という強力な関数を提供します.たとえば、2つ目の方法でjestを直接作成します.fn()を使用してトレースしましたが、関数の数が増えるとエラーが発生する可能性があります.
  • jest.mock()関数は、モジュール内の最初のパラメータに渡されるすべての関数を自動的に「ディレクトリ」(mock)関数に変更します.
  • jest.fn()

  • Jsetは、偽関数(mock functionton)を生成する関数である.
  • jest.fn()を用いてシミュレーションを行うことができるが,モジュールの方法や人為的な誤りを容易に修正できる.
  • pass!


    今はjestmockを使用してサービスモジュールに移動します.サービス層に関数を追加しても、柔軟にテストできます.

    コントローラ->サービス・レイヤ間のコールが正常に動作していることを確認しました.
    サービス層(ビジネスロジック)の開発を開始できます.では、今正式に開発すればいいですか.
    しかし、先ほどお話ししたように、ビジネスロジックはサービス層の責任です.では、何をテストしますか?ここまでですか?

    Refactor!


    コントローラのもう一つの役割、すなわちデータ検証を再設計しましょう.
    NestJSでは逆パイプラインがサポートされているため、ルータハンドラが実行される前に要求値を検証できます.
    どのような値があるかを知っていて、これらの値に基づいてベルの調整を行うことで、サービスエラーを減らすことができます.
    // dto
    export class CreateUserDto {
      email: string;
      password: string;
    }
    // controller
    @Controller('users')
    export class UsersController {
      constructor(private readonly usersService: UsersService) {}
      create(createUserDto: CreateUserDto) {
        this.usersService.create(createUserDto);
      }
    }
    // service
    @Injectable()
    export class UsersService {
      create(createUserDto: CreateUserDto) {
        return 'pass?';
      }
    }
    // test
    it('should call the service', () => {
        const createUserDto = {
          email: 'wanted',
          password: 'wecode',
        } as CreateUserDto;
        controller.create(createUserDto);
        expect(service.create).toHaveBeenCalled();
      });

    completed refactor?


    上記のように、DTOを追加してテストを実行し、テストに合格します.正しいデータ型が設定されていますが、通過したら終了しますか?
    いいえ.データ型を定義するためにDTOを設定しただけで、データは検証されていません.
    次に、データを検証するためのテストコードを追加します.

    データの再梱包を開始


    テスト環境では、TypeORMとMySQLを使用していますが、制限はありません.
    テスト環境と同じ環境を使用する場合は、NestJS公式マニュアルに従ってください.
    これで、DBが正常に動作している場合は、再テストと修復だけで済みます.
    電子メールの文字数が6文字未満であると仮定したvalidate関数を作成しました.
    // userEntity
    public static validate(email: string): boolean {
        const isValid = email && email.length <= 6;
        if (!isValid) {
          throw new Error('Invalid email');
        }
        return true;
      }
    // test (-----------> user.entity.spec.ts <----------)
    describe('user validate', () => {
      it('should return back a Error', () => {
        const email = 'wantedAndwecode';
        const spaceShipId = () => Users.validate(email);
    
        expect(spaceShipId).toThrow(Error);
      });
    
      it('should return back a valid email', () => {
        const email = 'wanted';
    
        expect(Users.validate(email)).toBeTruthy();
      });
    });
    わあ!Bellidationテストに合格しましたコントローラのテストまであまり時間が残っていません.

    では、今からコントローラにラベルを貼りましょうか?
    @Controller('users')
    export class UsersController {
      constructor(private readonly usersService: UsersService) {}
      create(createUserDto: CreateUserDto) {
        Users.validate(createUserDto.email);
        this.usersService.create(createUserDto);
      }
    }
    6文字の制限が検証のすべてである場合、上記のバージョンは正常に動作し、有効な値を得ることができます.
    しかし、私たちは単一責任の原則に違反しました.ルータプロセッサの役割はルータの役割です.
    幸いなことに、NestJSが提供しているValidation Pipe Lineがあります.DTOのデータ検証責任をパイプに委任する必要があります.
    公式文書のSchema validationを行います.
    ドキュメントに従って接続ライブラリをインストールしてください.

    必須


    tsconfig.jsonに追加しないと、正常に動作しません.
    "esModuleInterop": true
    では、配管の実施を始めましょう.
    // PipeTransform
    @Injectable()
    export class SpaceShipSaveRequestToSpaceShip implements PipeTransform {
      transform(value: any, metadata: ArgumentMetadata) {
        return value;
      }
    }
    // test
    describe('CreateUserDtoToUserPipe', () => {
      let transformer;
      beforeEach(() => {
        transformer = new CreateUserDtoToUserPipe();
      });
      it('should be defined', () => {
        expect(new CreateUserDtoToUserPipe()).toBeDefined();
      });
    
      it('should throw error if no body', () => {
        const response = () => transformer.transform({}, {});
        expect(response).toThrow(BadRequestException);
      });
    これでベッドサイドリクエストERRORテストに失敗しました

    ベッドヘッドリクエストERRORなどのベルを追加します.
    @Injectable()
    export class CreateUserDtoToUserPipe
      implements PipeTransform<CreateUserDto, Users>
    {
      transform(value: CreateUserDto, metadata: ArgumentMetadata): Users {
        const schema = Joi.object({
          email: Joi.string().min(3).max(12).required(),
          password: Joi.string().max(20).required(),
        });
    
        const { error } = schema.validate(value);
    
        if (error) {
          throw new BadRequestException('Validation failed');
        }
    
        Users.validate(value.email);
    
        const user = {
          email: value.email,
          password: value.password,
        } as Users;
        return user;
      }
    }
    最後に、次のように、DTO->ENTITYの変更後の値が同じかどうかをPipeで比較します.
    describe('CreateUserDtoToUserPipe', () => {
      let transformer;
      beforeEach(() => {
        transformer = new CreateUserDtoToUserPipe();
      });
      it('should be defined', () => {
        expect(new CreateUserDtoToUserPipe()).toBeDefined();
      });
    
      it('should throw error if no body', () => {
        const response = () => transformer.transform({}, {});
        expect(response).toThrow(BadRequestException);
      });
    
      it('should convert to valid User', () => {
        const createUserDto: CreateUserDto = {
          email: 'wanted',
          password: 'wecode',
        };
    
        const user = {
          email: 'wanted',
          password: 'wecode',
        } as Users;
    
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        const parsedUser = transformer.transform(createUserDto, {});
        expect(parsedUser).toEqual(user);
      });
    });

    completed refactor!!



    私たちはやっとコントローラのテストを完了しました.もちろん、Class-Validationは、Joiを使用するよりも有効性をより詳細に検証することはできません.
    使い勝手の良さから、NestJSバージョン推奨順で行った方が良いです.

    次に、サービス・レイヤのテストに戻ります。