【Golang】json.Marshal のエラーをテストする【json.MarshalIndent 含む失敗ケースの作成】

> Go 言語（以下 Golang）で、俺様関数内の `json.Marshal`/`json.MarshalIndent` を失敗させたい。つまり、**JSON 変換に失敗するテスト・ケースが欲しい**。

```go:テストしたい関数
type Foo int

func (f Foo) Dump(w io.Writer) (n int, err error) {
	if w == nil {
		return 0, errors.New("dump failed: the io.Writer w is nil")
	}

	byteJSON, err := json.Marshal(f)
	if err != nil {
        // ここのエラーに、たどり着けない
		return 0, errors.Wrap(err, "dump failed: can not marshal NodeLog to JSON")
	}

	return w.Write(byteJSON)
}
```
- [オンラインでユニット・テストをみる](https://go.dev/play/p/eJgx-pYQnvQ) | テストをパスできないケース @ Go Playground

Golang の [Marshal() のソース](https://cs.opensource.google/go/go/+/1ebc983000ed411a1c06f6b8a61770be1392e707:src/encoding/json/encode.go;l=157-169)を見ても、`Marshal()` の[引数に渡された型に問題がある場合にエラーを返す](https://cs.opensource.google/go/go/+/1ebc983000ed411a1c06f6b8a61770be1392e707:src/encoding/json/encode.go;l=321-333;drc=0c7fcf6bd1fd8df2bfae3a482f1261886f6313c1)ようになっています。`Foo` のインスタンスが作成された時点で型は決まってしまうため、`json.Marshal()` のエラー・ケースをテストできないのです。

`type Foo int` を `type Foo interface{}` などに変更できればいいのですが、やんごとなき事情でできません。

[「<code>"golang" json.Marshal テスト 失敗ケース</code>」でググって](https://www.google.com/search?q=%22golang%22+json.Marshal+%E3%83%86%E3%82%B9%E3%83%88+%E5%A4%B1%E6%95%97%E3%82%B1%E3%83%BC%E3%82%B9)も、ドンピシャの記事がなかったので自分のググラビリティとして。

## TL; DR <sub><sup><sub><sup>（今北産業）</sup></sub></sup></sub>

1. `monkey patching`（[モンキーパッチ](https://ja.wikipedia.org/wiki/%E3%83%A2%E3%83%B3%E3%82%AD%E3%83%BC%E3%83%91%E3%83%83%E3%83%81)）の手法を使う。
1. `json.Marshal` を変数に代入し、それを使う。<br>テスト時には、変数の中身をモック（擬似処理するもの）と入れ替えて、強制的にエラーを出力させる。
1. マスター、動くものをくれ (Go 1.18） 👇

```go:サンプル・ソース
package main

import (
	"bytes"
	"encoding/json"
	"io"
	"testing"

	"github.com/pkg/errors"
	"github.com/stretchr/testify/require"
)

// json.Marshal is a monkey patch of json.Marshal to ease testing.
var jsonMarshal = json.Marshal

// ----------------------------------------------------------------------------
//  Type: Foo
// ----------------------------------------------------------------------------

type Foo int

func (f Foo) Dump(w io.Writer) (n int, err error) {
	if w == nil {
		return 0, errors.New("dump failed: the io.Writer w is nil")
	}

	byteJSON, err := jsonMarshal(f) // <--- Monkey patched !!
	if err != nil {
		return 0, errors.Wrap(err, "dump failed: can not marshal NodeLog to JSON")
	}

	return w.Write(byteJSON)
}

// ----------------------------------------------------------------------------
//  Tests
// ----------------------------------------------------------------------------

// 正常ケース
func TestFoo_Dump_golden(t *testing.T) {
	bar := Foo(5963)
	b := bytes.Buffer{}

	_, err := bar.Dump(&b)

	require.NoError(t, err)
	require.Equal(t, "5963", b.String())
}

// 引数が nil だった場合
func TestFoo_Dump_arg_is_nil(t *testing.T) {
	bar := Foo(5963)

	n, err := bar.Dump(nil)

	require.Error(t, err, "nil input should be an error")
	require.Zero(t, n, "it should be zero on error")
	require.Contains(t, err.Error(),
		"dump failed: the io.Writer w is nil")
}

// マーシャリング（JSON 変換）に失敗した場合
func TestFoo_Dump_fail_marshaling(t *testing.T) {
	// Backup and defer recover
	oldJsonMarshal := jsonMarshal
	defer func() {
		jsonMarshal = oldJsonMarshal
	}()

	// Mock
	jsonMarshal = func(v any) ([]byte, error) {
		return nil, errors.New("forced error to marshal")
	}

	bar := new(Foo)

	b := bytes.Buffer{}

	n, err := bar.Dump(&b)

	require.Error(t, err, "nil input should be an error")
	require.Zero(t, n, "it should be zero on error")
	require.Contains(t, err.Error(),
		"forced error to marshal")
}
```
- [オンラインで動作をみる](https://go.dev/play/p/TrqFozVifJ4) @ Go Playground

## TS; DR

下記の `os.Exit` や `os.Args` などのモックと同じ手法です。

https://qiita.com/KEINOS/items/718c4237d8d0b3f4c26b

## 参考文献

- [encode_test.go](https://cs.opensource.google/go/go/+/1ebc983000ed411a1c06f6b8a61770be1392e707:src/encoding/json/encode_test.go;l=751-761) | json | encoding | src @ cs.opensource.google
- [How to write unit test for failure case of json.Marshall?](https://stackoverflow.com/questions/48898542/how-to-write-unit-test-for-failure-case-of-json-marshall) @ StackOverflow
- [Json unmarshal fails due to invalid characters](https://stackoverflow.com/questions/37294556/json-unmarshal-fails-due-to-invalid-characters) @ StackOverflow