ChatGPT に依頼して週末ハッカソンで書いたソースコードのテストコードを書いてもらった

# ChatGPT 盛り上がってますね。


https://openai.com/blog/chatgpt/

[過去に防災をテーマにしたハッカソンに参加したことがあります](https://qiita.com/t-kurasawa/items/52775f74282782023fbf)。一年以上前のことなので当時どんなソースコードを書いたかはうろ覚えです。ハッカソンあるあるなのですが、テストコードをほとんど書かなかったことだけ覚えています。

本記事では、話題の ChatGPT にテストコードを書いてもらいたいと思います。対象となるソースコードは以下です。

https://github.com/t-kurasawa/bichiku-map

# ChatGPT にテストコード作成を依頼する前の状態
週末ハッカソンで書いたテストコードは２つだけ。カバレッジは３１％程度です。
手動で動いていることは確認したものの、安心感はあまり無いですね。

ここから Chat GPT にテストコードを書いてもらってどこまでカバレッジが上がるのか試していきたいと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127260/9fe5e50f-d5b2-9309-20f0-1e84158d8af4.png)



```shell
$ npm run test 

> bichiku-map@1.0.0 test
> react-scripts test --verbose --silent --coverage --watchAll=false

 PASS  src/__test__/components/OpenStreetMap.test.tsx
  コンポーネントテスト
    ✓ OpenStreetMap (189 ms)

 PASS  src/__test__/pages/Dashboard.test.tsx
  画面テスト
    ✓ Dashboard (262 ms)

-----------------------------|---------|----------|---------|---------|-------------------------------
File                         | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------------------------|---------|----------|---------|---------|-------------------------------
All files                    |   31.34 |    18.42 |   25.25 |   31.17 |
 src                         |   27.77 |        0 |      25 |   23.52 |
  App.tsx                    |       0 |      100 |       0 |       0 | 9
  hooks.ts                   |     100 |      100 |     100 |     100 |
  index.tsx                  |       0 |      100 |     100 |       0 | 12-30
  reportWebVitals.ts         |       0 |        0 |       0 |       0 | 3-10
  store.ts                   |     100 |      100 |     100 |     100 |
  theme.ts                   |     100 |      100 |     100 |     100 |
 src/__mock__                |       0 |      100 |       0 |       0 |
  browser.ts                 |       0 |      100 |     100 |       0 | 4
  handlers.ts                |       0 |      100 |       0 |       0 | 4-21
 src/apis                    |   45.45 |      100 |       0 |   45.45 |
  evacuation-api.ts          |      50 |      100 |       0 |      50 | 5-6
  index.ts                   |       0 |        0 |       0 |       0 |
  stockpile-api.ts           |   42.85 |      100 |       0 |   42.85 | 5-6,10-11
 src/components              |   28.67 |    21.42 |   26.31 |   28.77 |
  ContributeStockpile.tsx    |       0 |        0 |       0 |       0 | 44-263
  Copyright.tsx              |      75 |       50 |   33.33 |      75 | 14,18
  EvacuationCenterDrawer.tsx |       0 |        0 |       0 |       0 | 21-69
  Header.tsx                 |    90.9 |    66.66 |   83.33 |    90.9 | 68
  LocationMarker.tsx         |     100 |       50 |   66.66 |     100 | 77
  MapController.tsx          |   57.14 |      100 |   33.33 |   57.14 | 14-18
  OpenStreetMap.tsx          |     100 |      100 |   66.66 |     100 |
  SideMenu.tsx               |     100 |      100 |     100 |     100 |
  Space.tsx                  |       0 |      100 |       0 |       0 | 2
  SpeedDial.tsx              |   53.33 |        0 |      60 |      50 | 32-41,65
  StockpileStatusEC.tsx      |       0 |        0 |       0 |       0 | 28-102
  StockpileType.tsx          |       0 |        0 |       0 |       0 | 26-39
  SubTitle.tsx               |       0 |      100 |       0 |       0 | 9
  TextBody.tsx               |       0 |      100 |       0 |       0 | 9
  TextBody2.tsx              |       0 |      100 |       0 |       0 | 9
  Title.tsx                  |       0 |      100 |       0 |       0 | 9
 src/pages                   |   21.73 |    16.66 |      40 |   21.73 |
  Contribution.tsx           |       0 |      100 |       0 |       0 | 7-8
  Dashboard.tsx              |     100 |      100 |     100 |     100 |
  EvacuationCenter.tsx       |       0 |        0 |       0 |       0 | 32-114
  FAQ.tsx                    |       0 |      100 |       0 |       0 | 9-10
  Template.tsx               |     100 |       50 |     100 |     100 | 16
 src/schema                  |       0 |        0 |       0 |       0 |
  index.ts                   |       0 |        0 |       0 |       0 |
 src/stores                  |      50 |      100 |   22.72 |      50 |
  evacuation-slice.ts        |   53.84 |      100 |   33.33 |   53.84 | 31-32,46-53
  openstreetmap-slice.ts     |   72.72 |      100 |      40 |   72.72 | 29-32,41
  stockpile-slice.ts         |   36.36 |      100 |    9.09 |   36.36 | 91-93,100-102,116-133,139,142
-----------------------------|---------|----------|---------|---------|-------------------------------
Test Suites: 2 passed, 2 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        8.949 s
```

# テストコードを ChatGTP と共に作っていく

## テストコードを依頼する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127260/008d466b-bf79-1d95-8300-9018a43523c1.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127260/44f7a412-1235-2147-8c2f-bcb79d0dc9c9.png)

moxios を使ってテストコードを書いてくれました。


```JavaScript

import axios from 'axios';
import moxios from 'moxios';
import evacuationApi from 'path/to/evacuationApi';

describe('fetchEvacuationCenter', () => {
  beforeEach(() => {
    moxios.install(axios);
  });

  afterEach(() => {
    moxios.uninstall(axios);
  });

  it('should fetch evacuation center data', async () => {
    const mockData = {
      name: 'Evacuation Center 1',
      location: '123 Main St',
      capacity: 100,
    };

    moxios.stubRequest(process.env.REACT_APP_BASE_URL + '/evacuation/center', {
      status: 200,
      response: mockData,
    });

    const response = await evacuationApi.fetchEvacuationCenter();

    expect(response.data).toEqual(mockData);
    expect(response.status).toBe(200);
  });
});

```


## テストコードを動かしてみる

### 1. ChatGPT が生成したテストコードをそのまま実行

:::note alert
moxios テストライブラリが入っていないためテスト失敗
:::

```shell
$ npm run test src/__test__/apis/evacuation-api.test.tsx

> bichiku-map@1.0.0 test
> react-scripts test --verbose --silent --coverage --watchAll=false "src/__test__/apis/evacuation-api.test.tsx"

 FAIL  src/__test__/apis/evacuation-api.test.tsx
  ● Test suite failed to run

    Cannot find module 'moxios' from 'src/__test__/apis/evacuation-api.test.tsx'

      1 | import axios from 'axios';
    > 2 | import moxios from 'moxios';
        | ^
      3 | import evacuationApi from 'path/to/evacuationApi';
      4 |
      5 | describe('fetchEvacuationCenter', () => {

      at Resolver.resolveModule (node_modules/jest-resolve/build/resolver.js:324:11)
      at Object.<anonymous> (src/__test__/apis/evacuation-api.test.tsx:2:1)
```

### 2. moxios というライブラリを追加して再実行。

:::note alert
import evacuationApi from 'path/to/evacuationApi';　のパスが通っていないためテスト失敗
:::


```shell
$ npm run test src/__test__/apis/evacuation-api.test.tsx

> bichiku-map@1.0.0 test
> react-scripts test --verbose --silent --coverage --watchAll=false "src/__test__/apis/evacuation-api.test.tsx"

 FAIL  src/__test__/apis/evacuation-api.test.tsx
  ● Test suite failed to run

    Cannot find module 'path/to/evacuationApi' from 'src/__test__/apis/evacuation-api.test.tsx'

      1 | import axios from 'axios';
      2 | import moxios from 'moxios';
    > 3 | import evacuationApi from 'path/to/evacuationApi';
        | ^
      4 |
      5 | describe('fetchEvacuationCenter', () => {
      6 |   beforeEach(() => {

      at Resolver.resolveModule (node_modules/jest-resolve/build/resolver.js:324:11)
      at Object.<anonymous> (src/__test__/apis/evacuation-api.test.tsx:3:1)
```

### 3. パスを修正しテスト再実行

:::note info
テスト成功。evacuation-api.ts のカバレッジは100%になりました。
簡単なソースコードとはいえ ChatGPT に依頼してから３つの作業でテストコードができました。すばらしいですね。
:::

```shell
$ npm run test src/__test__/apis/evacuation-api.test.tsx

> bichiku-map@1.0.0 test
> react-scripts test --verbose --silent --coverage --watchAll=false "src/__test__/apis/evacuation-api.test.tsx"

 PASS  src/__test__/apis/evacuation-api.test.tsx
  fetchEvacuationCenter

-----------------------------|---------|----------|---------|---------|-------------------
File                         | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------------------------|---------|----------|---------|---------|-------------------
All files                    |    1.58 |        0 |    1.01 |    1.61 |
  evacuation-api.ts          |     100 |      100 |     100 |     100 |
-----------------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.953 s

```


## React の小さめのコンポーネントのテストコードを依頼してみる

テスト対象のソースコード

```React
import { useState } from 'react';
import { Global } from '@emotion/react';

import {
  Avatar,
  Box,
  Card,
  CardHeader,
  CardContent,
  Typography,
  SwipeableDrawer,
} from '@mui/material';

import { styled } from '@mui/material/styles';
import { grey } from '@mui/material/colors';

import { StockpileStatusEC } from 'components/StockpileStatusEC';

import escape from 'assets/images/icons/escape-301x194px-04A040.svg';

const drawerBleeding = 56;

interface Props {
  value?: any;
  open: boolean;
}

const Root = styled('div')(({ theme }) => ({
  height: '100%',
  backgroundColor:
    theme.palette.mode === 'light' ? '#fff' : theme.palette.background.default,
}));

const StyledBox = styled(Box)(({ theme }) => ({
  backgroundColor: theme.palette.mode === 'light' ? '#fff' : grey[800],
}));

const Puller = styled(Box)(({ theme }) => ({
  width: 30,
  height: 6,
  backgroundColor: theme.palette.mode === 'light' ? grey[300] : grey[900],
  borderRadius: 3,
  position: 'absolute',
  top: 8,
  left: 'calc(50% - 15px)',
}));

const EvacuationDrawer = (props: Props) => {
  const [open, setOpen] = useState(false);

  return (
    <Root>
      <Global
        styles={{
          '.MuiDrawer-root > .MuiPaper-root': {
            height: `calc(85% - ${drawerBleeding}px)`,
            overflow: 'visible',
          },
        }}
      />
      <Avatar src={escape} variant="square" />
      <SwipeableDrawer
        anchor="bottom"
        open={open}
        onClose={() => {
          setOpen(false);
        }}
        onOpen={() => {
          setOpen(true);
        }}
        swipeAreaWidth={drawerBleeding}
        disableSwipeToOpen={false}
        ModalProps={{
          keepMounted: true,
        }}
      >
        <StyledBox
          sx={{
            position: 'absolute',
            top: -32,
            borderTopLeftRadius: 8,
            borderTopRightRadius: 8,
            visibility: 'visible',
            right: 0,
            left: 0,
          }}
        >
          <Puller />
          <Typography sx={{ p: 2, color: 'text.secondary' }} />
        </StyledBox>
        <StyledBox
          sx={{
            px: 2,
            pb: 2,
            height: '100%',
            overflow: 'auto',
          }}
        >
          <Card sx={{ maxWidth: 'auto' }}>
            <CardHeader
              avatar={<Avatar src={escape} variant="square" />}
              title={props.value.避難所_名称 + '（' + props.value.住所 + '）'}
            />
            <CardContent>
              <StockpileStatusEC ec={props.value} />
            </CardContent>
          </Card>
        </StyledBox>
      </SwipeableDrawer>
    </Root>
  );
};

export default EvacuationDrawer;

```

React Testing Library でテストコードを書いてくれました。
先程と同じくライブラリの追加やパスの修正は必要です。

```React
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import EvacuationDrawer from 'components/EvacuationCenterDrawer';

const value = {
  避難所_名称: 'Test Shelter',
  住所: 'Test Address',
  床数: 100,
  入所者数: 50,
  空き床数: 50,
  緊急物資: {
    食料: '100',
    水: '200',
    毛布: '50',
  },
};

test('EvacuationDrawer should render the correct information', () => {
  render(<EvacuationDrawer open value={value} />);
  expect(screen.getByText('Test Shelter')).toBeInTheDocument();
  expect(screen.getByText('Test Address')).toBeInTheDocument();
  expect(screen.getByText('床数: 100')).toBeInTheDocument();
  expect(screen.getByText('入所者数: 50')).toBeInTheDocument();
  expect(screen.getByText('空き床数: 50')).toBeInTheDocument();
  expect(screen.getByText('食料: 100')).toBeInTheDocument();
  expect(screen.getByText('水: 200')).toBeInTheDocument();
  expect(screen.getByText('毛布: 50')).toBeInTheDocument();
});

test('EvacuationDrawer should open and close', () => {
  render(<EvacuationDrawer open={false} value={value} />);
  expect(screen.queryByText('Test Shelter')).not.toBeInTheDocument();
  render(<EvacuationDrawer open value={value} />);
  expect(screen.getByText('Test Shelter')).toBeInTheDocument();
});

test('EvacuationDrawer should have the correct avatar', () => {
  render(<EvacuationDrawer open value={value} />);
  expect(screen.getByRole('img')).toHaveAttribute(
    'src',
    'escape-301x194px-04A040.svg'
  );
});

test('EvacuationDrawer should have the correct puller color', () => {
  render(<EvacuationDrawer open value={value} />);
  const puller = screen.getByTestId('puller');
  expect(puller).toHaveStyle('background-color: #e0e0e0');
});

```



### 1. テスト実行

```shell
$ npm run test src/__test__/components/EvacuationCenterDrawer.test.tsx

> bichiku-map@1.0.0 test
> react-scripts test --verbose --silent --coverage --watchAll=false "src/__test__/components/EvacuationCenterDrawer.test.tsx"

 FAIL  src/__test__/components/EvacuationCenterDrawer.test.tsx
  ✕ EvacuationDrawer should render the correct information (207 ms)
  ✕ EvacuationDrawer should open and close (24 ms)
  ✕ EvacuationDrawer should have the correct avatar (28 ms)
  ✕ EvacuationDrawer should have the correct puller color (23 ms)

  ● EvacuationDrawer should render the correct information

    could not find react-redux context value; please ensure the component is wrapped in a <Provider>

      3 |
      4 | // Use throughout your app instead of plain `useDispatch` and `useSelector`
    > 5 | export const useAppDispatch = () => useDispatch<AppDispatch>();
        |                                     ^
      6 | export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
```

:::note alert
テスト失敗しました。テストコードが悪いのか、アプリケーションソースコードが悪いのか切り分けていきます。
:::


### 2. せっかくなので ChatGPT にバグを探してもらいます


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127260/4fbfdfd4-784f-308c-072f-e5b9798bb94f.png)


> 申し訳ありませんが、このコードには具体的なバグを見つけることができません。これはReactコンポーネントで、Material-UIとEmotionライブラリを利用して、スワイプ可能なドロワーコンポーネントを作成しています。値やopen propが渡されるのを期待して、その値を使ってドロワー内のカードに何らかの情報を表示させているように見えます。このコンポーネントにバグがあるかどうかは、もっと詳細な情報がないと、はっきり言えません。


残念ながらピンポイントでバグを教えてくれることはありませんでした。
聞き方の問題なのかこの辺りが限界なのかはもう少し使いこなしていくとわかりそうです。

基本に立ち返りスタックトレースを見ると、依存関係先の redux をテストコードに定義できていないので失敗しているようです。
このように依存関係があるソースコードに対しては、依存関係先の情報が無い限りはテストコードを書いてくれることはありませんでした。当然ですね。



# ChatGPT にテストコード作成を依頼した後の状態

### テストコードを全く書いていない、筆が乗らない人は ChatGPT にテストコードを依頼するといいかも

Qiita を書きながらですが、ChatGPT に依頼してテストコードを書きました。１時間半で結構増えたと思います。
ChatGPT に依頼するための前提の技術知識や提出されたコードの正しさを理解したり修正作業は必要ですが、大変便利なサポートツールだと思います。
みなさまも是非一度触ってみてください。

:::note info
作業時間：　１時間半

テストコード　２つ　　→　６つ
カバレッジ　　３１％　→　４６％
バグ　　　　　０件　　→　１３件検出
:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127260/8d46ff41-7b7f-36a1-9c68-44f3146a8cc5.png)



```shell
$ npm run test

> bichiku-map@1.0.0 test
> react-scripts test --verbose --silent --coverage --watchAll=false

 PASS  src/__test__/apis/evacuation-api.test.tsx
  fetchEvacuationCenter
    ✓ should fetch evacuation center data (48 ms)

 PASS  src/__test__/apis/stockpile-api.test.tsx
  fetchStockpileType
    ✓ should fetch stockpile type data (48 ms)
  fetchStockpileStatusEC
    ✓ should fetch stockpile status by evacuation center (9 ms)

 PASS  src/__test__/apis/index.test.tsx
  apis
    ✓ should export evacuationApi (3 ms)
    ✓ should export stockpileApi (1 ms)

 FAIL  src/__test__/components/ContributeStockpile.test.tsx
  OpenStreetMap component
    ✕ renders a MapContainer (2 ms)
    ✕ renders a TileLayer
    ✕ renders a MapController
    ✕ renders a LocationMarker (1 ms)
    ✕ initializes the map with the correct center and zoom
    ✕ calls whenReady function (1 ms)
    ✕ calls whenCreated function (1 ms)

 FAIL  src/__test__/components/OpenStreetMap.test.tsx
  ✕ renders the map (205 ms)
  ✕ initial position is correct (19 ms)
  ✕ zoom level is correct (23 ms)
  ✕ location marker is rendered (17 ms)
  ✕ mapcontroller is rendered (18 ms)
  ✕ whenReady and whenCreated functions are called (23 ms)

 PASS  src/__test__/pages/Dashboard.test.tsx
  画面テスト
    ✓ Dashboard (323 ms)

-----------------------------|---------|----------|---------|---------|------------------------
File                         | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------------------------|---------|----------|---------|---------|------------------------
All files                    |   46.03 |    31.57 |   44.44 |   44.93 |
 src                         |   27.77 |        0 |      25 |   23.52 |
  App.tsx                    |       0 |      100 |       0 |       0 | 9
  hooks.ts                   |     100 |      100 |     100 |     100 |
  index.tsx                  |       0 |      100 |     100 |       0 | 12-30
  reportWebVitals.ts         |       0 |        0 |       0 |       0 | 3-10
  store.ts                   |     100 |      100 |     100 |     100 |
  theme.ts                   |     100 |      100 |     100 |     100 |
 src/__mock__                |       0 |      100 |       0 |       0 |
  browser.ts                 |       0 |      100 |     100 |       0 | 4
  handlers.ts                |       0 |      100 |       0 |       0 | 4-21
 src/apis                    |     100 |      100 |     100 |     100 |
  evacuation-api.ts          |     100 |      100 |     100 |     100 |
  index.ts                   |       0 |        0 |       0 |       0 |
  stockpile-api.ts           |     100 |      100 |     100 |     100 |
 src/components              |   46.15 |    39.28 |   43.85 |    44.6 |
  ContributeStockpile.tsx    |       0 |        0 |       0 |       0 | 44-263
  Copyright.tsx              |      75 |       50 |   33.33 |      75 | 14,18
  EvacuationCenterDrawer.tsx |   83.33 |       50 |   66.66 |   77.77 | 66-69
  Header.tsx                 |    90.9 |    66.66 |   83.33 |    90.9 | 68
  LocationMarker.tsx         |     100 |       50 |   66.66 |     100 | 77
  MapController.tsx          |   57.14 |      100 |   33.33 |   57.14 | 14-18
  OpenStreetMap.tsx          |     100 |      100 |     100 |     100 |
  SideMenu.tsx               |     100 |      100 |     100 |     100 |
  Space.tsx                  |       0 |      100 |       0 |       0 | 2
  SpeedDial.tsx              |   53.33 |        0 |      60 |      50 | 32-41,65
  StockpileStatusEC.tsx      |    87.5 |    33.33 |      80 |    87.5 | 42-48
  StockpileType.tsx          |       0 |        0 |       0 |       0 | 26-39
  SubTitle.tsx               |       0 |      100 |       0 |       0 | 9
  TextBody.tsx               |       0 |      100 |       0 |       0 | 9
  TextBody2.tsx              |       0 |      100 |       0 |       0 | 9
  Title.tsx                  |     100 |      100 |     100 |     100 |
 src/pages                   |   21.73 |    16.66 |      40 |   21.73 |
  Contribution.tsx           |       0 |      100 |       0 |       0 | 7-8
  Dashboard.tsx              |     100 |      100 |     100 |     100 |
  EvacuationCenter.tsx       |       0 |        0 |       0 |       0 | 32-114
  FAQ.tsx                    |       0 |      100 |       0 |       0 | 9-10
  Template.tsx               |     100 |       50 |     100 |     100 | 16
 src/schema                  |       0 |        0 |       0 |       0 |
  index.ts                   |       0 |        0 |       0 |       0 |
 src/stores                  |   63.04 |      100 |      50 |   63.04 |
  evacuation-slice.ts        |   53.84 |      100 |   33.33 |   53.84 | 31-32,46-53
  openstreetmap-slice.ts     |   72.72 |      100 |      40 |   72.72 | 29-32,41
  stockpile-slice.ts         |   63.63 |      100 |   63.63 |   63.63 | 93,102,119-123,129-133
-----------------------------|---------|----------|---------|---------|------------------------
Test Suites: 3 failed, 4 passed, 7 total
Tests:       16 failed, 7 passed, 23 total
Snapshots:   0 total
Time:        10.454 s
```