Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

TypeScriptとJestではじめる AWS製サーバーレス REST API のユニットテ...

Yusuke Wada
December 14, 2019

TypeScriptとJestではじめる AWS製サーバーレス REST API のユニットテスト・E2Eテスト #serverlessfukuoka #serverlessdays / Serverless testing using TypeScript and Jest

この発表では、サーバーレスアプリケーションのユニットテストとE2Eテストを書くときの考え方とその手段について述べます。テストについて議論するときは、アプリケーション設計についても述べなければなりません。

発表の中でまず、サーバーレスアプリケーションは、レイヤ・機能別にコードを分類できることを示します。その後、分類しおたコードに対し、Jestでモック化し、テスト対象を抽出してテストするユニットテストを行います。このときモック対象の戦略についても話します。

最後にE2Eテストの有効性と実行方法を述べます。

Yusuke Wada

December 14, 2019
Tweet

More Decks by Yusuke Wada

Other Decks in Technology

Transcript

  1. େࠓ೔ࢀߟʹ͍͍ͤͯͨͩͨ͞৘ใ ࿨ా୎ਓ, 2017/6/2 AWS Dev Day Tokyo 2017 IUUQTTQFBLFSEFDLDPNUXBEBUFTUBCMFMBNCEBXPSLJOHF⒎FDUJWFMZXJUIMFHBDZMBNCEB Testable

    Lambda Working Effectivery with Legacy Lambda Clean Architecture ୡਓʹֶͿιϑτ΢ΣΞͷ ߏ଄ͱઃܭ Robert C. Martin ςετۦಈ։ൃ Kent Beck, ࿨ా୎ਓ
  2. 5JQTϞοΫԽʹ͸+FTUͷTQZ0O ͕໾ཱͭ describe('Clear ticket use case', () => { test('run

    clear ticket', async (): Promise<void> => { const en = ExchangeNotification.of('123456'); const getTicketSpy = jest .spyOn(ticket, 'getTicket') .mockResolvedValue(searchTicket); const clearTicketSpy = jest .spyOn(ticket, 'clearTicket') .mockResolvedValue('Created:UT'); const result = await clearingTicketUseCase(en); expect(result).toEqual({ result: 'Created', }); expect(getTicketSpy).toHaveBeenCalledWith( en.ticketNo,en.shoriTime, ); expect(clearTicketSpy).toHaveBeenCalledWith(searchTicket); }); });
  3. 5JQTϞοΫԽʹ͸+FTUͷTQZ0O ͕໾ཱͭ describe('Clear ticket use case', () => { test('run

    clear ticket', async (): Promise<void> => { const en = ExchangeNotification.of('123456'); const getTicketSpy = jest .spyOn(ticket, 'getTicket') .mockResolvedValue(searchTicket); const clearTicketSpy = jest .spyOn(ticket, 'clearTicket') .mockResolvedValue('Created:UT'); const result = await clearingTicketUseCase(en); expect(result).toEqual({ result: 'Created', }); expect(getTicketSpy).toHaveBeenCalledWith( en.ticketNo,en.shoriTime, ); expect(clearTicketSpy).toHaveBeenCalledWith(searchTicket); }); }); ͜ΕͰϩʔΧϧ࣮ߦՄೳ
  4. 5JQTϐϡΞϩδοΫͷ໢ཏʹ͸1BSBNFUFSJ[FE describe('NotNullValidator', () => { test.each` name | value |

    expected ${'address'} | ${'tokyo'} | ${{ isValid: true }} ${'first name'} | ${''} | ${{ isValid: true }} ${'first name'} | ${null} | ${{ isValid: false }} `( 'input $value, validation result expected: $expected.isValid', ({ name, value, expected }) => { const suc = new NotNullValidator(name, value); expect(suc.validate()).toEqual({ isValid: expected.isValid, }); }, ); });
  5. 5JQTϐϡΞϩδοΫͷ໢ཏʹ͸1BSBNFUFSJ[FE describe('NotNullValidator', () => { test.each` name | value |

    expected ${'address'} | ${'tokyo'} | ${{ isValid: true }} ${'first name'} | ${''} | ${{ isValid: true }} ${'first name'} | ${null} | ${{ isValid: false }} `( 'input $value, validation result expected: $expected.isValid', ({ name, value, expected }) => { const suc = new NotNullValidator(name, value); expect(suc.validate()).toEqual({ isValid: expected.isValid, }); }, ); }); ೖྗɾظ଴஋ϖΞΛςʔϒϧܗࣜͰఆٛ
  6. 8FC"1*࢓ࣄͷύλʔϯ ϦΫΤετϨεϙϯε describe('exchange API', (): void => { test('clear ticket',

    async (): void => { const exchangeNotificationUrl = 'http://ticket-system.jp/ticket/clear'; const request = { shori_id: '01234567890', }; const response = await Axios.post( exchangeNotificationUrl, qs.stringify(request), ); expect(response.data).toEqual('{ "response": "ok" }'); }); });
  7. 8FC"1*࢓ࣄͷύλʔϯ ϦΫΤετϨεϙϯε describe('exchange API', (): void => { test('clear ticket',

    async (): void => { const exchangeNotificationUrl = 'http://ticket-system.jp/ticket/clear'; const request = { shori_id: '01234567890', }; const response = await Axios.post( exchangeNotificationUrl, qs.stringify(request), ); expect(response.data).toEqual('{ "response": "ok" }'); }); }); ϨεϙϯεϘσΟΛ֬ೝ
  8. 8FC"1*࢓ࣄͷύλʔϯ ϦΫΤετσʔλอଘ describe('exchange API', (): void => { test('clear ticket',

    async (): void => { // Axios request … // const param = { TableName: 'ticket-info', Key: { shoriId: '01234567890' }, }; const dynamoData = await dynamo.getItem(param).promsie(); expect(dynamoData).toEqual({ shoriId: request.shori_id, clear: true, }); }); });
  9. 8FC"1*࢓ࣄͷύλʔϯ ϦΫΤετσʔλอଘ describe('exchange API', (): void => { test('clear ticket',

    async (): void => { // Axios request … // const param = { TableName: 'ticket-info', Key: { shoriId: '01234567890' }, }; const dynamoData = await dynamo.getItem(param).promsie(); expect(dynamoData).toEqual({ shoriId: request.shori_id, clear: true, }); }); }); อଘ͞ΕͨσʔλΛ֬ೝ
  10. ྫɿ4ʹอଘͨ͠ॲཧه࿥Λࢀরͯ֬͠ೝ͢Δ describe('exchange API', (): void => { test('clear ticket', async

    (): Promise<void> => { const exchangeNotificationUrl = 'http://ticket-system.jp/ticket/clear'; const request = { shori_id: '01234567890', }; const response = await Axios.post( exchangeNotificationUrl, qs.stringify(request), ); expect(response.data).toEqual('{ "response": "ok" }'); const param = { Bucket: 'ticket-info', Key: request.shori_id, }; const s3Obj = await s3.getObject(param).promsie(); const reocrd = JSON.parse(s3Obj.Body!.toString()); expect(reocrd).toEqual({ monitor: 'Created', }); }); });
  11. ྫɿ4ʹอଘͨ͠ॲཧه࿥Λࢀরͯ֬͠ೝ͢Δ describe('exchange API', (): void => { test('clear ticket', async

    (): Promise<void> => { const exchangeNotificationUrl = 'http://ticket-system.jp/ticket/clear'; const request = { shori_id: '01234567890', }; const response = await Axios.post( exchangeNotificationUrl, qs.stringify(request), ); expect(response.data).toEqual('{ "response": "ok" }'); const param = { Bucket: 'ticket-info', Key: request.shori_id, }; const s3Obj = await s3.getObject(param).promsie(); const reocrd = JSON.parse(s3Obj.Body!.toString()); expect(reocrd).toEqual({ monitor: 'Created', }); }); });
  12. ྫɿ4ʹอଘͨ͠ॲཧه࿥Λࢀরͯ֬͠ೝ͢Δ describe('exchange API', (): void => { test('clear ticket', async

    (): Promise<void> => { const exchangeNotificationUrl = 'http://ticket-system.jp/ticket/clear'; const request = { shori_id: '01234567890', }; const response = await Axios.post( exchangeNotificationUrl, qs.stringify(request), ); expect(response.data).toEqual('{ "response": "ok" }'); const param = { Bucket: 'ticket-info', Key: request.shori_id, }; const s3Obj = await s3.getObject(param).promsie(); const reocrd = JSON.parse(s3Obj.Body!.toString()); expect(reocrd).toEqual({ monitor: 'Created', }); }); }); ֎෦"1*͔Β$SFBUFE͕ ฦ͖ͬͯͨ͜ͱΛه࿥ɺ֬ೝ ※࣮ࡍʹ͸஗ԆͱϦτϥΠ͕ඞཁ
  13. ϢχοτςετͷΞϓϩʔν )VNCMF0CKFDU ͷϢχοτςετ ϐϡΞϩδοΫͷϢχοτςετ ਖ਼ৗܥͷ&&ςετ ҟৗܥΛؚΉ&&ςετ 6 5 & 

    & JSONม׵ϛε ϩδοΫߟྀ࿙Ε AWSߏ੒ϛε
 IAMϛε ࢓༷ͷෆ۩߹ ςετ͠΍͍͢ϐϡΞϩδοΫͱͦΕҎ֎Λ෼ׂͨ͠
  14. ϢχοτςετͷΞϓϩʔν )VNCMF0CKFDU ͷϢχοτςετ ϐϡΞϩδοΫͷϢχοτςετ ਖ਼ৗܥͷ&&ςετ ҟৗܥΛؚΉ&&ςετ 6 5 & 

    & JSONม׵ϛε ϩδοΫߟྀ࿙Ε AWSߏ੒ϛε
 IAMϛε ࢓༷ͷෆ۩߹ +FTUͷػೳΛ׆༻ͯ͠ޮ཰ԽͰ͖Δ ɾspyOn() ʹΑΔϞοΫ ɾparameterized test
  15. &&ςετ )VNCMF0CKFDU ͷϢχοτςετ ϐϡΞϩδοΫͷϢχοτςετ ਖ਼ৗܥͷ&&ςετ ҟৗܥΛؚΉ&&ςετ 6 5 & 

    & JSONม׵ϛε ϩδοΫߟྀ࿙Ε AWSߏ੒ϛε
 IAMϛε ࢓༷ͷෆ۩߹ ·ͣ͸Ұຊ௨͢
  16. &&ςετ )VNCMF0CKFDU ͷϢχοτςετ ϐϡΞϩδοΫͷϢχοτςετ ਖ਼ৗܥͷ&&ςετ ҟৗܥΛؚΉ&&ςετ 6 5 & 

    & JSONม׵ϛε ϩδοΫߟྀ࿙Ε AWSߏ੒ϛε
 IAMϛε ࢓༷ͷෆ۩߹ ύλʔϯʹ߹Θ࣮ͤͯࢪ ɾϦΫΤετ-Ϩεϙϯε ɾϦΫΤετ-σʔλอଘ ɾϦΫΤετ-αʔϏείʔϧ
  17. ·ͱΊ )VNCMF0CKFDU ͷϢχοτςετ ϐϡΞϩδοΫͷϢχοτςετ ਖ਼ৗܥͷ&&ςετ ҟৗܥΛؚΉ&&ςετ 6 5 & 

    & JSONม׵ϛε ϩδοΫߟྀ࿙Ε AWSߏ੒ϛε
 IAMϛε ࢓༷ͷෆ۩߹ ͜ͷ;ͨͭɺίεύ˕Ͱ͸͡Ί΍͍͢ʂ˞΋ͪΖΜ࠷ޙ͸શ෦΍Δ
  18. େࠓ೔ࢀߟʹ͍͍ͤͯͨͩͨ͞৘ใ ࿨ా୎ਓ, 2017/6/2 AWS Dev Day Tokyo 2017 IUUQTTQFBLFSEFDLDPNUXBEBUFTUBCMFMBNCEBXPSLJOHF⒎FDUJWFMZXJUIMFHBDZMBNCEB Testable

    Lambda Working Effectivery with Legacy Lambda Clean Architecture ୡਓʹֶͿιϑτ΢ΣΞͷ ߏ଄ͱઃܭ Robert C. Martin ςετۦಈ։ൃ Kent Beck, ࿨ా୎ਓ