My External Storage

Nov 17, 2020 - 5 minute read - Comments - terraform go

自作Terraform Providerのユニットテストの書き方

公式チュートリアルには載っていなかったので、自作Terraform Providerを作るときのユニットテストの書き方をメモしておく。
なお、最初にコメントしておくと今回の記事はかなり説明を省略しているので各Providerにコミットしたことがあるか自作Providerを作った人じゃないとわからなそう…

TL;DR

// schema.Resource.ReadContextに設定する関数のテスト
func Test_dataSourceYourObjectRead(t *testing.T) {
  // *schema.ProviderData.ConfigureContextFuncの戻り値にしているオブジェクト
  m := providerConfigureReturnedObject
  // *schema.Provider.DataSourcesMapに設定するResource
  d := dataSourceYourObject().TestResourceData()

  got := dataSourceYourObjectRead(context.TODO(), d, m)
  if diff := cmp.Diff(got, tt.want); diff != "" {
    t.Errorf("dataSourceYourObjectRead: (-got +want)\n%s", diff)
  }
}

Terraform Providerの概要

Terraformはプロパイダーを経由して各種リソースを操作する。 AWSプロパイダーやGCPプロパイダー経由でマネージドサービスを管理している方も多いだろう。

TerraformはIaaS以外もプロパイダーさえあれば宣言的に管理できる。 公式で提供されているプロパイダーだけでもこれだけある。

また、Goが書けるならばSDKを使って自作のプロパイダーを作成し、レジストリに公開することもできる、

自作プロパイダーは次の公式ガイドを読みながら実装することができる。

日本語資料がよいならソフトウェアデザイン11月号の「作品で魅せるGoプログラミング」に解説が載っている。

自作Provider実装時の動作確認のめんどくささ

ホビープロジェクトとしてTerraformの自作Providerを作っている。
Providerはterraformコマンドが呼び出すサププロセスとなる。
そのため、作るのは簡単だが動作確認がめんどくさい。
さきほど紹介した資料ではどちらにも一度ビルドしてTerraform経由で起動する動作確認方法が記載されている。

$ pwd
/Users/budougumi0617/go/src/github.com/budougumi0617/terraform-provider-hashicups
# 一度ビルドする
$ make install
go build -o terraform-provider-hashicups
mv terraform-provider-hashicups ~/.terraform.d/plugins/hashicorp.com/edu/hashicups/0.2/darwin_amd64
# tfファイルを用意したディレクトリに移動する
$ cd examples
# ビルドしたら必ずinitしないといけない
$ terraform init
# ここまでやってやっとapplyして動作確認ができる
$ terraform apply --auto-approve

この方法だと毎回ビルドをしたあと異なるディレクトリでterraform init && terraform applyをする必要があり時間もかかる。
また、デバッガのアタッチも難しい。そのためユニットテストで動作確認をしたい。

自作Providerのユニットテストを書く

自作Providerの実装構造

Terraformで自作プロバイダーを実装する流れはざっくり次の通り。

  • データあるいはリソースの構造を定義する
  • データあるいはリソースに対して、CRUD操作にひとつひとつ対応する関数を実装する

CRUD操作に対応するそれぞれの関数は次のシグネチャを持つ関数として実装する。

// See Resource documentation.
type CreateContextFunc func(context.Context, *ResourceData, interface{}) diag.Diagnostics

// See Resource documentation.
type ReadContextFunc func(context.Context, *ResourceData, interface{}) diag.Diagnostics

// See Resource documentation.
type UpdateContextFunc func(context.Context, *ResourceData, interface{}) diag.Diagnostics

// See Resource documentation.
type DeleteContextFunc func(context.Context, *ResourceData, interface{}) diag.Diagnostics

(つまり同じシグネチャなのだが、)この関数の引数にわたすオブジェクトを用意すれば各XxxxContextFuncに対応するCRUD操作のユニットテストが実行できる。

テスト対象の自作データの定義

今回テストの対象になるデータリソースの定義を確認する。

import (
  "context"
  "fmt"

  pixela "github.com/ebc-2in2crc/pixela4go"

  "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
  "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func dataSourceGraphs() *schema.Resource {
  return &schema.Resource{
    ReadContext: dataSourceGraphsRead,
    Schema: map[string]*schema.Schema{
      "graphs": {
        Type:     schema.TypeList,
        Computed: true,
        Elem: &schema.Resource{
          Schema: map[string]*schema.Schema{
            "id": {
              Type:     schema.TypeString,
              Computed: true,
            },
            "name": {
              Type:     schema.TypeString,
              Computed: true,
            },
            // ...
          },
        },
      },
    },
  }
}

// この関数をテストしたい
func dataSourceGraphsRead(
  ctx context.Context, d *schema.ResourceData, m interface{},
  ) diag.Diagnostics {
  client := m.(*pixela.Client)
  var diags diag.Diagnostics
  // Do anything...
  return diags
}

また、ProviderのConfigureContextFuncに登録している関数は次の通り。

import (
  "context"
  "fmt"

  pixela "github.com/ebc-2in2crc/pixela4go"
  "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
  "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func providerConfigure(
  _ context.Context, d *schema.ResourceData,
  ) (interface{}, diag.Diagnostics) {
  un := d.Get("username").(string)
  if un == "" {
    return nil, diag.FromErr(fmt.Errorf("not find username"))
  }

  token := d.Get("token").(string)
  if token == "" {
    return nil, diag.FromErr(fmt.Errorf("not find token"))
  }

  return pixela.New(un, token), diag.Diagnostics{}
}

dataSourceGraphsRead関数をテストするには、*schema.ResourceDataオブジェクトを入手する必要がある。
*schema.ResourceDataオブジェクトは、dataSourceGraphs関数の戻り値である*schema.Resourceオブジェクトから入手することができる。

func (*Resource) TestResourceData
func (r *Resource) TestResourceData() *ResourceData
TestResourceData Yields a ResourceData filled with this resource’s schema for use in unit testing

TODO: May be able to be removed with the above ResourceData function.

第1引数のcontextは適当なcontextでよく、第3引数はConfigureContextFuncに登録するproviderConfigure関数の戻り値で良い。
今回の場合だと第3引数はproviderConfigure関数が返す*pixela.Clientオブジェクトになる。

これらを踏まえると、次のようにtestを書くことができる。

package pixela

import (
  "context"
  "os"
  "testing"

  pixela "github.com/ebc-2in2crc/pixela4go"
  "github.com/google/go-cmp/cmp"
  "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
)

func Test_dataSourceGraphsRead(t *testing.T) {
  tests := [...]struct {
    name string
    want diag.Diagnostics
  }{
    {
      name: "confirmClientResponse",
      want: nil,
    },
  }
  for _, tt := range tests {
    usename := os.Getenv("PIXELA_USERNAME")
    token := os.Getenv("PIXELA_TOKEN")
    if usename == "" || token == "" {
      t.SkipNow()
    }
    t.Run(tt.name, func(t *testing.T) {
      m := pixela.New(usename, token)
      d := dataSourceGraphs().TestResourceData()
      got := dataSourceGraphsRead(context.TODO(), d, m)
      if diff := cmp.Diff(got, tt.want); diff != "" {
        t.Errorf("dataSourceGraphsRead: (-got +want)\n%s", diff)
      }
    })
  }
}

これでunittestとして動作確認ができるようになった。 また、デバッガを接続して動作確認もできるので、データの流れ方も確認することができる。

終わりに

前提知識の補足をほとんど書いていないが、terraformのProviderのユニットテストを書く方法をまとめた。
terraform-provider-awsなどを確認すると、もっとテスト用の関数を多用しているので、他にもどんなテストが書けるか読んでおきたい。

参考

関連記事