Goのテスト実践：NewspaperモデルのCRUDとJSONマッピングを網羅的にテストする方法

## はじめに
Goでアプリケーション開発をする際、データモデルの信頼性を確保するためのテストは欠かせません。本記事では、`Newspaper`モデルのCRUD操作（Create, Read, Update, Delete）とJSONマッピングのテストを網羅的に実装する方法を紹介します。`testify/suite`を活用して、読みやすく再利用性の高いテストコードを作成する手順を一行ずつ解説します。


## 記事の目次
1. **`Newspaper`モデルの概要**
2. **テストコード全体**
3. **コード解説：一行ずつ徹底解説**
4. **テストケースの実行と期待される結果**

## 1. `Newspaper`モデルの概要

`Newspaper`モデルは以下のような構造を持ち、新聞記事を管理します。

- **CRUD操作**：データベースを操作して記事を作成、取得、更新、削除します。
- **JSONマッピング**：構造体の内容をJSON形式で返す機能を備えています。

```go
package models

import (
	"encoding/json"

	"go-api-newspaper/api"
)

type Newspaper struct {
	ID          int
	Title       string
	ColumnName  string
}

// 構造体をjsonに変換する
func (a *Newspaper) MarshalJSON() ([]byte, error) {
	return json.Marshal(&api.NewspaperResponse{ // api.NewspaperResponse という別の構造体にデータを詰め替えている
		Id:          a.ID,
		Title:       a.Title,
		ColumnName:  a.ColumnName,
	})
}

func CreateNewspaper(title string, columnName string) (*Newspaper, error) {
	newspaper := &Newspaper{
		Title:       title,
		ColumnName:  columnName,
	}
	if err := DB.Create(newspaper).Error; err != nil {
		return nil, err
	}
	return newspaper, nil
}

func GetNewspaper(ID int) (*Newspaper, error) {
	var newspaper = Newspaper{}
	if err := DB.First(&newspaper, ID).Error; err != nil {
		return nil, err
	}
	return &newspaper, nil
}

func (a *Newspaper) Save() error {
	if err := DB.Save(&a).Error; err != nil {
		return err
	}
	return nil
}

func (a *Newspaper) Delete() error {
	if err := DB.Where("id = ?", &a.ID).Delete(&a).Error; err != nil {
		return err
	}
	return nil
}
```

## 2. テストコード全体

以下が`Newspaper`モデルのテストコードです。

```go
package models_test  

import (  
	"fmt"  
	"strings"  
	"testing"  

	"github.com/stretchr/testify/suite"  
	"gorm.io/gorm"  

	"go-api-newspaper/app/models"  
	"go-api-newspaper/pkg/tester"  
)  

type NewspaperTestSuite struct {  
	tester.DBSQLiteSuite  
	originalDB *gorm.DB  
}  

func TestNewspaperTestSuite(t *testing.T) {  
	suite.Run(t, new(NewspaperTestSuite))  
}  

func (suite *NewspaperTestSuite) SetupSuite() {  
	suite.DBSQLiteSuite.SetupSuite()  
	suite.originalDB = models.DB  
}  

func (suite *NewspaperTestSuite) AfterTest(suiteName, testName string) {  
	models.DB = suite.originalDB  
}  

func (suite *NewspaperTestSuite) TestNewspaper() {  
	createdNewspaper, err := models.CreateNewspaper("Test", "sports")  
	suite.Assert().Nil(err)  
	suite.Assert().Equal("Test", createdNewspaper.Title)  
	suite.Assert().Equal("sports", createdNewspaper.ColumnName)  

	getNewspaper, err := models.GetNewspaper(createdNewspaper.ID)  
	suite.Assert().Nil(err)  
	suite.Assert().Equal("Test", getNewspaper.Title)  
	suite.Assert().Equal("sports", getNewspaper.ColumnName)  

	getNewspaper.Title = "updated"  
	err = getNewspaper.Save()  
	suite.Assert().Nil(err)  
	updatedNewspaper, err := models.GetNewspaper(createdNewspaper.ID)  
	suite.Assert().Nil(err)  
	suite.Assert().Equal("updated", updatedNewspaper.Title)  
	suite.Assert().Equal("sports", updatedNewspaper.ColumnName)  

	err = updatedNewspaper.Delete()  
	suite.Assert().Nil(err)  
	deletedNewspaper, err := models.GetNewspaper(updatedNewspaper.ID)  
	suite.Assert().Nil(deletedNewspaper)  
	suite.Assert().True(strings.Contains("record not found", err.Error()))  
}  

func (suite *NewspaperTestSuite) TestNewspaperMarshal() {  
	newspaper := models.Newspaper{  
		Title:      "Test",  
		ColumnName: "sports",  
	}  
	newspaperJSON, err := newspaper.MarshalJSON()  
	suite.Assert().Nil(err)  
	suite.Assert().JSONEq(fmt.Sprintf(`{  
		"columnName":"sports",  
		"id":0,  
        "title":"Test"  
    }`), string(newspaperJSON))  
}  
```

## 3. コード解説：一行ずつ徹底解説

以下で、コード全体をセクションごとに解説します。

### パッケージとインポート
```go
package models_test  
```
- **`models_test`**：`models`パッケージのテストコード用パッケージ。プロダクションコードとは分離されます。
```go
import (  
	"fmt"  
	"strings"  
	"testing"  
	"github.com/stretchr/testify/suite"  
	"gorm.io/gorm"  
	"go-api-newspaper/app/models"  
	"go-api-newspaper/pkg/tester"  
)  
```
- **`fmt`と`strings`**：文字列操作やフォーマット出力に利用。
- **`testify/suite`**：テストスイートの作成と管理を行うためのライブラリ。
- **`gorm`**：GoのORMツール。データベース操作を簡易化します。
- **`models`と`tester`**：アプリケーションのモデルとテストユーティリティをインポート。

### テストスイートの定義と初期化
```go
type NewspaperTestSuite struct {  
	tester.DBSQLiteSuite  
	originalDB *gorm.DB  
}  
```
- **`DBSQLiteSuite`**：テスト用データベースをセットアップするスイート。
- **`originalDB`**：元のデータベース接続を保持します。
```go
func TestNewspaperTestSuite(t *testing.T) {  
	suite.Run(t, new(NewspaperTestSuite))  
}  
```
- **`TestNewspaperTestSuite`**：テストランナー。`NewspaperTestSuite`に定義されたテストケースを実行します。


### Setupとクリーンアップ
```go
func (suite *NewspaperTestSuite) SetupSuite() {  
	suite.DBSQLiteSuite.SetupSuite()  
	suite.originalDB = models.DB  
}  
```
- **`SetupSuite`**：全テストの前にSQLiteデータベースをセットアップ。
```go
func (suite *NewspaperTestSuite) AfterTest(suiteName, testName string) {  
	models.DB = suite.originalDB  
}  
```
- **`AfterTest`**：各テスト後にデータベース接続を元に戻します。


### CRUDテストケース
```go
func (suite *NewspaperTestSuite) TestNewspaper() {  
	createdNewspaper, err := models.CreateNewspaper("Test", "sports")  
	suite.Assert().Nil(err)  
	...  
}  
```
- **`CreateNewspaper`**：新聞記事を作成。
- **`GetNewspaper`**：作成した記事を取得し、タイトルやコラム名を確認。
- **`Save`**：タイトルを更新し、変更が正しく反映されているか確認。
- **`Delete`**：記事を削除し、エラーメッセージを確認。


### JSONマッピングのテストケース
```go
func (suite *NewspaperTestSuite) TestNewspaperMarshal() {  
	newspaper := models.Newspaper{  
		Title: "Test",  
		ColumnName: "sports",  
	}  
	...  
}  
```
- **`MarshalJSON`**：構造体をJSON形式に変換し、期待されるJSONと一致するかを確認。


## 4. テストケースの実行と期待される結果

このテストスイートを実行すると、以下を検証できます：

1. CRUD操作が正しく動作する。
2. JSONマッピングが期待どおりに動作する。
3. エラー処理が適切に行われている。

これにより、`Newspaper`モデルの信頼性を高めることができます。

## まとめ

以上が、`Newspaper`モデルのテストを網羅的に実装する方法です。このテストを活用して、安心してアプリケーションを開発しましょう！