Closed8

(試行)GoのユニットテストをChatGPTへ書かせてみる

taitotaito

自分は趣味でGoのアプリケーションを作っている(本業はGoではない)。本業はが忙しくなるとどうしても開発に時間が取れない。そして結構今忙しい。

趣味で作ってるアプリケーションではユニットテストを極力書くようにしている。本業ではどうしても開発速度が優先されてしまう。さまざまなステークホルダーからの要求により、システムを理想的な状態で運営していくことは難しい。ユニットテストも省略されることがよくある(問題だよなぁとは思いつつ、他組織でも同じ課題に直面しているはず)。この本業でのフラストレーションを解消するために趣味開発では開発速度やコストを度外視で、やりたいことをやりたいようにやる、アプリケーションを理想的な状態で運営していくことを目指している。ユニットテストを極力書きたいという動機は、アプリケーションを理想な状態で運営していきたいと思っていることから来ている。

ユニットテストをChatGPTに書かせることができれば、趣味開発(本業が忙しく時間が取れない)も本業(忙しいい)も両方ハッピーになるじゃないか。ということでやってみた。

taitotaito

注意

以前、ユニットテストをChatGPTへ書かせる試行に何度か取り組んだことはあるが、これはちょっと難しいな、と思った。ただ、Promptの与え方次第なのかとも思った。

今回、改めて挑戦するわけだが、うまくいく保証はどこにもない。

したがって、この記事がどこに着地するのか。わからない。

taitotaito

今回、下記のコードのユニットテストを、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
}
taitotaito

ポイントは

  • HTTPリクエストをモックライブラリgockを使用して書いてもらうこと。
  • カバレッジを100%にすること。
  • TDT形式でテストコードを書いてもらうこと。

下記がPrompt

taitotaito

下記の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%にすること。
taitotaito

動かないテストコードを返してくる。これは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にして欲しい。
    • 指示漏れなので、指示を追加する。
taitotaito

下記の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%になるように、テストケースを追加してください。

taitotaito

想像以上に難しい・・・

ちょっと断念。

このスクラップは2023/12/07にクローズされました