KEINOS
@KEINOS (KEINOS)

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

【Golang】filepath.Abs で error を発生させたい(テスト用のエラー・ケースが欲しい)

解決したいこと

filepath.Abs() を使った関数のテストで、Abs() がエラーを返すパターンが作れないか悩んでいます。

何を与えてもパースできちゃうのでパスしてしまうのです。エラーが欲しいのに。

if path, err := filepath.Abs("<エラーにしたいパス>"); err != nil {
    // Do something on failure
    // ここにたどりつかない
} else {
    // Do something on success
}

微妙〜にカバレッジが網羅できず気持ちが悪いのでアイデアがないかお伺いしたいです。

  • テストは Docker の(golang:1.17-alpine) コンテナ上で実行しているので Linux 環境で error を返してくれるだけで嬉しいです。

該当するソースコード

package main

import (
	"path/filepath"
	"testing"
)

func DoSomething(path string) (string, error) {
	pathAbs, err := filepath.Abs(path)
	if err != nil {
		return "", err // ここにたどり着けない
	}

	return pathAbs, nil
}

func TestDoSomething(t *testing.T) {
	for _, test := range []struct {
		input     string
		expectOut string
		expectErr bool
	}{
		{"..", "/", false},
		{"<ここが欲しい>", "", true},
	} {
		actualOut, err := DoSomething(test.input)

		// Case assert error
		if test.expectErr && err == nil {
			t.Fatalf("it should return an error: %v", test.input)
		}

		// Case assert equal
		if test.expectOut != actualOut {
			t.Fatalf("it should return: %#v, got: %#v", test.input, actualOut)
		}
	}
}

【結論】
filepath.Abs() はエラーを返さないため、filepathAbs := filepath.Abs と変数に代入しておき、テスト時にエラーを返す関数を一時的に割り当てる、モンキー・パッチする方法に落ち着きました。(回答参照)

0

1Answer

path.go @ Go 1.17.3 のソースを見ると、os.Getwd() がエラーを返さない限り filepath.Abs() もエラーを返さない感じです。

src/path/filepath/path.go
func unixAbs(path string) (string, error) {
    if IsAbs(path) {
        return Clean(path), nil
    }
    wd, err := os.Getwd()
    if err != nil {
        return "", err
    }
    return Join(wd, path), nil
}

そのため、現時点では filepath.Abs() の動作をモックしやすいように、変数に代入したものを使い、テスト時に一時的に入れ替える方法しか思いつきません。「そんなことせんでも○○○すればエラーになりまっせ」というのがあればベストなのですが。

package main

import (
    "path/filepath"
    "testing"

    "github.com/pkg/errors"
)

// FilePathAbs は filepath.Abs のコピーです。テスト時の動作をモックし
// やすくするためのものです。
var FilePathAbs = filepath.Abs

func DoSomething(path string) (string, error) {
    pathAbs, err := FilePathAbs(path)
    if err != nil {
        return "", err
    }

    return pathAbs, nil
}

// 正常系テスト
func TestDoSomething_golden(t *testing.T) {
    for _, test := range []struct {
        input     string
        expectOut string
    }{
        {"..", "/"},
        {"", "/"},
    } {
        actualOut, err := DoSomething(test.input)

        // Assert NoError
        if err != nil {
            t.Fatalf("it should not return an error: %v", err)
        }

        // Case assert equal
        if test.expectOut != actualOut {
            t.Fatalf("it should return: %#v, got: %#v", test.input, actualOut)
        }
    }
}

// 異常系テスト
func TestDoSomething_forced_error(t *testing.T) {
    // モック前のバックアップとリカバリ
    oldFilePathAbs := FilePathAbs
    defer func() {
        FilePathAbs = oldFilePathAbs
    }()

    // モック
    FilePathAbs = func(path string) (string, error) {
        return "", errors.New("dummy error")
    }

    // エラー時のテスト
    result, err := DoSomething("")

    // Require empty
    if result != "" {
        t.Fatalf("on error the value should be empty. Got: %#v", result)
    }

    // Require error
    if err == nil {
        t.Fatalf("it should return an error but got nil")
    }
}

0Like

Your answer might help someone💌