ChatGPT 盛り上がってますね。
過去に防災をテーマにしたハッカソンに参加したことがあります。一年以上前のことなので当時どんなソースコードを書いたかはうろ覚えです。ハッカソンあるあるなのですが、テストコードをほとんど書かなかったことだけ覚えています。
本記事では、話題の ChatGPT にテストコードを書いてもらいたいと思います。対象となるソースコードは以下です。
ChatGPT にテストコード作成を依頼する前の状態
週末ハッカソンで書いたテストコードは2つだけ。カバレッジは31%程度です。
手動で動いていることは確認したものの、安心感はあまり無いですね。
ここから Chat GPT にテストコードを書いてもらってどこまでカバレッジが上がるのか試していきたいと思います。
$ 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 と共に作っていく
テストコードを依頼する
moxios を使ってテストコードを書いてくれました。
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 が生成したテストコードをそのまま実行
moxios テストライブラリが入っていないためテスト失敗
$ 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 というライブラリを追加して再実行。
import evacuationApi from 'path/to/evacuationApi'; のパスが通っていないためテスト失敗
$ 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. パスを修正しテスト再実行
テスト成功。evacuation-api.ts のカバレッジは100%になりました。
簡単なソースコードとはいえ ChatGPT に依頼してから3つの作業でテストコードができました。すばらしいですね。
$ 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 の小さめのコンポーネントのテストコードを依頼してみる
テスト対象のソースコード
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 でテストコードを書いてくれました。
先程と同じくライブラリの追加やパスの修正は必要です。
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. テスト実行
$ 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;
テスト失敗しました。テストコードが悪いのか、アプリケーションソースコードが悪いのか切り分けていきます。
2. せっかくなので ChatGPT にバグを探してもらいます
申し訳ありませんが、このコードには具体的なバグを見つけることができません。これはReactコンポーネントで、Material-UIとEmotionライブラリを利用して、スワイプ可能なドロワーコンポーネントを作成しています。値やopen propが渡されるのを期待して、その値を使ってドロワー内のカードに何らかの情報を表示させているように見えます。このコンポーネントにバグがあるかどうかは、もっと詳細な情報がないと、はっきり言えません。
残念ながらピンポイントでバグを教えてくれることはありませんでした。
聞き方の問題なのかこの辺りが限界なのかはもう少し使いこなしていくとわかりそうです。
基本に立ち返りスタックトレースを見ると、依存関係先の redux をテストコードに定義できていないので失敗しているようです。
このように依存関係があるソースコードに対しては、依存関係先の情報が無い限りはテストコードを書いてくれることはありませんでした。当然ですね。
ChatGPT にテストコード作成を依頼した後の状態
テストコードを全く書いていない、筆が乗らない人は ChatGPT にテストコードを依頼するといいかも
Qiita を書きながらですが、ChatGPT に依頼してテストコードを書きました。1時間半で結構増えたと思います。
ChatGPT に依頼するための前提の技術知識や提出されたコードの正しさを理解したり修正作業は必要ですが、大変便利なサポートツールだと思います。
みなさまも是非一度触ってみてください。
作業時間: 1時間半
テストコード 2つ → 6つ
カバレッジ 31% → 46%
バグ 0件 → 13件検出
$ 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