(試行)GoのユニットテストをChatGPTへ書かせてみる
自分は趣味でGoのアプリケーションを作っている(本業はGoではない)。本業はが忙しくなるとどうしても開発に時間が取れない。そして結構今忙しい。
趣味で作ってるアプリケーションではユニットテストを極力書くようにしている。本業ではどうしても開発速度が優先されてしまう。さまざまなステークホルダーからの要求により、システムを理想的な状態で運営していくことは難しい。ユニットテストも省略されることがよくある(問題だよなぁとは思いつつ、他組織でも同じ課題に直面しているはず)。この本業でのフラストレーションを解消するために趣味開発では開発速度やコストを度外視で、やりたいことをやりたいようにやる、アプリケーションを理想的な状態で運営していくことを目指している。ユニットテストを極力書きたいという動機は、アプリケーションを理想な状態で運営していきたいと思っていることから来ている。
ユニットテストをChatGPTに書かせることができれば、趣味開発(本業が忙しく時間が取れない)も本業(忙しいい)も両方ハッピーになるじゃないか。ということでやってみた。
注意
以前、ユニットテストをChatGPTへ書かせる試行に何度か取り組んだことはあるが、これはちょっと難しいな、と思った。ただ、Promptの与え方次第なのかとも思った。
今回、改めて挑戦するわけだが、うまくいく保証はどこにもない。
したがって、この記事がどこに着地するのか。わからない。
今回、下記のコードのユニットテストを、ChatGPTに書いてもらいます。
package fetcherimpl
import (
"context"
"io"
"net/http"
"net/url"
"slices"
"github.com/suzuito/sandbox2-go/common/terrors"
"github.com/suzuito/sandbox2-go/crawler/internal/infra/internal/factory"
"github.com/suzuito/sandbox2-go/crawler/pkg/entity/argument"
"github.com/suzuito/sandbox2-go/crawler/pkg/entity/crawler"
)
type FetcherHTTP struct {
Cli *http.Client
StatusCodesSuccess []int
}
func (t *FetcherHTTP) Do(ctx context.Context, w io.Writer, input crawler.CrawlerInputData) error {
urlString, exists := input["URL"]
if !exists {
return terrors.Wrapf("input[\"URL\"] not found in input")
}
method, exists := input["Method"]
if !exists {
method = http.MethodGet
}
methodAsString := ""
switch v := method.(type) {
case string:
methodAsString = v
default:
return terrors.Wrapf("input[\"Method\"] must be string in input")
}
u, err := url.Parse(urlString.(string))
if err != nil {
return terrors.Wrap(err)
}
req, _ := http.NewRequestWithContext(ctx, methodAsString, u.String(), nil)
res, err := t.Cli.Do(req)
if err != nil {
return terrors.Wrap(err)
}
defer res.Body.Close()
if !slices.Contains(t.StatusCodesSuccess, res.StatusCode) {
body, _ := io.ReadAll(res.Body)
status := res.StatusCode
return terrors.Wrapf("HTTP error : status=%d body=%s", status, body)
}
if _, err := io.Copy(w, res.Body); err != nil {
return terrors.Wrapf("Failed to io.Copy: %+v", err)
}
return nil
}
ポイントは
- HTTPリクエストをモックライブラリgockを使用して書いてもらうこと。
- カバレッジを100%にすること。
- TDT形式でテストコードを書いてもらうこと。
下記がPrompt
下記の3つのバッククォートで囲まれたソースコードのユニットテストを書いて欲しいです。
Do関数のユニットテストだけを書いて欲しいです。
package fetcherimpl
import (
"context"
"io"
"net/http"
"net/url"
"slices"
"github.com/suzuito/sandbox2-go/common/terrors"
"github.com/suzuito/sandbox2-go/crawler/internal/infra/internal/factory"
"github.com/suzuito/sandbox2-go/crawler/pkg/entity/argument"
"github.com/suzuito/sandbox2-go/crawler/pkg/entity/crawler"
)
type FetcherHTTP struct {
Cli *http.Client
StatusCodesSuccess []int
}
func (t *FetcherHTTP) ID() crawler.FetcherID {
return crawler.FetcherID("fetcher_http")
}
func (t *FetcherHTTP) Do(ctx context.Context, w io.Writer, input crawler.CrawlerInputData) error {
urlString, exists := input["URL"]
if !exists {
return terrors.Wrapf("input[\"URL\"] not found in input")
}
method, exists := input["Method"]
if !exists {
method = http.MethodGet
}
methodAsString := ""
switch v := method.(type) {
case string:
methodAsString = v
default:
return terrors.Wrapf("input[\"Method\"] must be string in input")
}
u, err := url.Parse(urlString.(string))
if err != nil {
return terrors.Wrap(err)
}
req, _ := http.NewRequestWithContext(ctx, methodAsString, u.String(), nil)
res, err := t.Cli.Do(req)
if err != nil {
return terrors.Wrap(err)
}
defer res.Body.Close()
if !slices.Contains(t.StatusCodesSuccess, res.StatusCode) {
body, _ := io.ReadAll(res.Body)
status := res.StatusCode
return terrors.Wrapf("HTTP error : status=%d body=%s", status, body)
}
if _, err := io.Copy(w, res.Body); err != nil {
return terrors.Wrapf("Failed to io.Copy: %+v", err)
}
return nil
}
func NewFetcherHTTP(def *crawler.FetcherDefinition, args *factory.NewFuncFetcherArgument) (crawler.Fetcher, error) {
f := FetcherHTTP{}
if f.ID() != def.ID {
return nil, factory.ErrNoMatchedFetcherID
}
f.Cli = args.HTTPClient
statusCodesSuccess, err := argument.GetFromArgumentDefinition[[]int](def.Argument, "StatusCodesSuccess")
if err != nil {
return nil, terrors.Wrap(err)
}
f.StatusCodesSuccess = statusCodesSuccess
return &f, nil
}
下記の制約を守ってください。
- ユニットテスト関数の名前をTestFetcherHTTPDoにすること。
- 実際にHTTPリクエストを送信することはせず、gockライブラリを使ってモックすること。
- Table Driven Testの形式で記述すること。
下記は努力目標です。
- カヴァレッジを100%にすること。
動かないテストコードを返してくる。これはChatGPTあるあるです。
package fetcherimpl_test
import (
"context"
"io/ioutil"
"net/http"
"testing"
"github.com/h2non/gock"
"github.com/suzuito/sandbox2-go/crawler/internal/infra/internal/factory"
"github.com/suzuito/sandbox2-go/crawler/pkg/entity/crawler"
"github.com/suzuito/sandbox2-go/crawler/pkg/infra/fetcherimpl"
)
func TestFetcherHTTPDo(t *testing.T) {
tests := []struct {
name string
input crawler.CrawlerInputData
statusCode int
responseBody string
expectedErr bool
expectedOutput string
}{
{
name: "Successful GET Request",
input: crawler.CrawlerInputData{
"URL": "https://example.com/success",
"Method": "GET",
},
statusCode: http.StatusOK,
responseBody: "Successful Response",
expectedErr: false,
expectedOutput: "Successful Response",
},
{
name: "Error GET Request",
input: crawler.CrawlerInputData{
"URL": "https://example.com/error",
"Method": "GET",
},
statusCode: http.StatusNotFound,
responseBody: "Not Found",
expectedErr: true,
expectedOutput: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// テスト用のHTTPリクエストとレスポンスをモック
gock.New(tt.input["URL"].(string)).
Method(tt.input["Method"].(string)).
Reply(tt.statusCode).
BodyString(tt.responseBody)
// テスト対象のFetcherHTTPを作成
fetcher := fetcherimpl.FetcherHTTP{
Cli: &http.Client{},
StatusCodesSuccess: []int{http.StatusOK},
}
// テスト用のWriterを作成
writer := ioutil.Discard
// テストを実行
err := fetcher.Do(context.Background(), writer, tt.input)
// エラーが期待通りかどうかを検証
if (err != nil) != tt.expectedErr {
t.Errorf("Expected error: %v, got error: %v", tt.expectedErr, err)
}
// 出力が期待通りかどうかを検証
if writer.(*ioutil.Discard) != ioutil.Discard && tt.expectedOutput != writer.(*ioutil.Discard).String() {
t.Errorf("Expected output: %s, got output: %s", tt.expectedOutput, writer.(*ioutil.Discard).String())
}
// モックをリセット
gock.Off()
})
}
}
なぜ動かない?
- package 名が fetcherimpl_test になってる。正しくは fetcherimplにして欲しい。
- 指示漏れなので、指示を追加する。
下記の3つのバッククォートで囲まれたソースコードのNewFetcherHTTP関数のユニットテストだけを書いて欲しいです。
package fetcherimpl
import (
"context"
"io"
"net/http"
"net/url"
"slices"
"github.com/suzuito/sandbox2-go/common/terrors"
"github.com/suzuito/sandbox2-go/crawler/internal/infra/internal/factory"
"github.com/suzuito/sandbox2-go/crawler/pkg/entity/argument"
"github.com/suzuito/sandbox2-go/crawler/pkg/entity/crawler"
)
type FetcherHTTP struct {
Cli *http.Client
StatusCodesSuccess []int
}
func (t *FetcherHTTP) ID() crawler.FetcherID {
return crawler.FetcherID("fetcher_http")
}
func (t *FetcherHTTP) Do(ctx context.Context, w io.Writer, input crawler.CrawlerInputData) error {
urlString, exists := input["URL"]
if !exists {
return terrors.Wrapf("input[\"URL\"] not found in input")
}
method, exists := input["Method"]
if !exists {
method = http.MethodGet
}
methodAsString := ""
switch v := method.(type) {
case string:
methodAsString = v
default:
return terrors.Wrapf("input[\"Method\"] must be string in input")
}
u, err := url.Parse(urlString.(string))
if err != nil {
return terrors.Wrap(err)
}
req, _ := http.NewRequestWithContext(ctx, methodAsString, u.String(), nil)
res, err := t.Cli.Do(req)
if err != nil {
return terrors.Wrap(err)
}
defer res.Body.Close()
if !slices.Contains(t.StatusCodesSuccess, res.StatusCode) {
body, _ := io.ReadAll(res.Body)
status := res.StatusCode
return terrors.Wrapf("HTTP error : status=%d body=%s", status, body)
}
if _, err := io.Copy(w, res.Body); err != nil {
return terrors.Wrapf("Failed to io.Copy: %+v", err)
}
return nil
}
func NewFetcherHTTP(def *crawler.FetcherDefinition, args *factory.NewFuncFetcherArgument) (crawler.Fetcher, error) {
f := FetcherHTTP{}
if f.ID() != def.ID {
return nil, factory.ErrNoMatchedFetcherID
}
f.Cli = args.HTTPClient
statusCodesSuccess, err := argument.GetFromArgumentDefinition[[]int](def.Argument, "StatusCodesSuccess")
if err != nil {
return nil, terrors.Wrap(err)
}
f.StatusCodesSuccess = statusCodesSuccess
return &f, nil
}
下記のバッククォートで囲まれたソースコードが、書き途中のテストコードです。
package fetcherimpl
import (
"bytes"
"context"
"errors"
"net/http"
"testing"
"github.com/h2non/gock"
"github.com/suzuito/sandbox2-go/common/test_helper"
"github.com/suzuito/sandbox2-go/crawler/internal/infra/internal/factory"
"github.com/suzuito/sandbox2-go/crawler/pkg/entity/argument"
"github.com/suzuito/sandbox2-go/crawler/pkg/entity/crawler"
)
func TestNewFetcherHTTP(t *testing.T) {
testCases := []struct {
desc string
inputDef crawler.FetcherDefinition
inputArgs factory.NewFuncFetcherArgument
expectedError string
}{
{
desc: "Success",
inputDef: crawler.FetcherDefinition{
ID: "fetcher_http",
Argument: argument.ArgumentDefinition{
"StatusCodesSuccess": []int{http.StatusOK},
},
},
inputArgs: factory.NewFuncFetcherArgument{},
},
}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
_, err := NewFetcherHTTP(&tC.inputDef, &tC.inputArgs)
test_helper.AssertError(t, tC.expectedError, err)
})
}
}
下記の制約を守ってください。
カヴァレッジが100%になるように、テストケースを追加してください。
想像以上に難しい・・・
ちょっと断念。