🌑

Test data sourcesを利用してLaunchDarklyの振る舞いを制御する

2024/09/24に公開

はじめに

こんにちは。CastingONEでバックエンドを担当してるHamoroと申します。

弊社はFeatureFlagの運用基盤としてLaunchDarklyを導入しておりますが、FeatureFlagの評価値によってアプリケーションの振る舞いが期待通りになっているかをテストしたいと思い、良い感じに実装することができないかを調べてみました。その中で、LaunchDarklyのテスト用SDKで提供されているTest data sourcesを利用すると良さそうだったため今回は簡単にやり方をまとめました。

FeatureFlagとLaunchDarkly

FeatureFlagはアプリケーションコード内で実装されるif-else文のことで、その評価によって振る舞いを分けることができます。それにより、アプリケーションのデプロイとリリースを別々のタイミングで実行できるようになり、カナリアリリースのような特定のユーザーにだけリリースするみたいなことも可能になります。

LaunchDarklyはFeatureFlagを管理するためのプラットフォームです。提供されているSDKやCLIなどエコシステムが充実しており、柔軟に開発に組み込むことが可能となっています。
また、LaunchDarklyはOpenFeatureにも対応しており、いくつかの言語ではOpenFeature用のSDKも提供しています。ちなみに2024年9月時点ではGoのSDKは非公式のものしかありませんでしたが、その辺りのエコシステムはOpenFeatureのサイトで確認することができます。
https://openfeature.dev/ecosystem/?instant_search[refinementList][vendor][0]=LaunchDarkly

FeatureFlagの振る舞いをモックする

FeatureFlagをアプリケーションに組み込んでいると、往々にしてどこかでFeatureFlagが含まれている関数のテストをする場面に出くわすと思います。テスト実行時にも実際のFeatureFlagサーバーに問い合わせをしてしまうと、実行のたびにネットワーク通信が発生しFlakyになりがちです。その際に実際のサーバーを使うのではなくモックして振る舞いを固定することで、振る舞いを安定させることができるため、高速で信頼性の高いテストを実行することができます。

Test data sourcesを使う

値を柔軟に制御したいとなった時に、Test data sourcesを利用するやり方がお手軽で楽だったので軽く紹介したいと思います。

前提としてLaunchDarklyのSDKを利用する際は、必ずdata sourceを持っている必要があります。本番で稼働しているアプリケーションであれば、これは通常LaunchDarklyのサーバーに接続されてますので、そこに問い合わせて取得しています。OpenFeatureの用語を拝借するならFeature Flagging Serviceというやつですね。以下の図はOpenFeatureのドキュメントから持ってきました。

Feature Flagging Service

このdata sourceなんですが、SDKの初期実行時にテスト用のdata sourceに差し替えることができます。それによりSDKの振る舞いをモックすることが可能になり、フラグがどのように評価されるかを柔軟に制御することが可能になります。

実装サンプル(Go SDK v6.0)

import (
	"time"

	ld "github.com/launchdarkly/go-server-sdk/v6"
	"github.com/launchdarkly/go-server-sdk/v6/ldcomponents"
	"github.com/launchdarkly/go-server-sdk/v6/testhelpers/ldtestdata"
)

func Init(sdkKey string, opts ...ldClientConfigOption) error {
	var config ld.Config
	for _, opt := range opts {
		opt(&config)
	}

	timeout := time.Second * 60
	cli, err := ld.MakeCustomClient(sdkKey, config, timeout)
	if err != nil {
		return err
	}
	// Do something with the client
}

type ldClientConfigOption func(*ld.Config)

func WithDataSourceForTest() ldClientConfigOption {
    td := ldtestdata.DataSource()

    td.Update(td.Flag("some-boolean-flag").VariationForAll(true))

    return func(conf *ld.Config) {
        conf.DataSource = td
    }
}

func WithNoEvents() ldClientConfigOption {
	return func(conf *ld.Config) {
		conf.Events = ldcomponents.NoEvents()
	}
}

まず Init という関数を定義し LaunchDarkly の Client を作成しています。その際に、ConfigOption を引数でもらうようにしています。2つオプションを返す関数を定義しており、1つずつみていきたいと思います。

data sourcesを差し替える

func WithDataSourceForTest() ldClientConfigOption {
    td := ldtestdata.DataSource()

    td.Update(td.Flag("some-boolean-flag").VariationForAll(true))

    return func(conf *ld.Config) {
        conf.DataSource = td
    }
}

DataSource()はTestDataSource構造体のインスタンスを内部で作成しており、このインスタンスに対して行われた変更はこのdata sourcesを利用している全てのLaunchDarkly Clientに伝搬します。FeatureFlagの値を更新するためにはUpdate()を使うことができます。

例で使用しているVariationForAll()は全てのコンテキストに対して同一のバリエーションを指定することができますが、コンテキストによって細かく制御することも可能です。

例)tenant_idが10のユーザーだけfalseにする(Boolean Flag)

td.Update(td.Flag("some-boolean-flag").
    IfMatchContext("company", "tenant_id", ldvalue.Int(10)).
    ThenReturn(true).FallthroughVariation(false),
)

例)tenant_idが10のユーザーにだけIndex 1の値を返す(Multivariate Flag)

td.Update(td.Flag("some-multivariate-flag").
    IfMatchContext("company", "tenant_id", ldvalue.Int(10)).
    ThenReturnIndex(1).FallthroughVariationIndex(0),
)

分析イベントを送信しないようにする

func WithNoEvents() ldClientConfigOption {
	return func(conf *ld.Config) {
		conf.Events = ldcomponents.NoEvents()
	}
}

SDKは実行時に分析イベントをサーバーに送信しようと試みます。特にサーバーに接続する必要がない場合は、分析イベントが送信されないようにするオプションも用意されています。そうなるとLaunchDarklyへの接続がないため、有効なSDKキーを使用する必要はありません。SDKキー自体は引き続き必要ですが、任意の文字列を使用できます。

// サーバーに接続しないので、SDKキーは任意の文字列を使用できる
client, _ := ld.MakeCustomClient("sdk-key-123abc", config, timeout)

最後に

今回はTest data sourcesを利用したテストのやり方についてご紹介しました。以下のドキュメントには他の様々なやり方が紹介されていますので、興味があれば合わせてご参照ください。
https://docs.launchdarkly.com/guides/flags/testing-code

弊社はFeatureFlagを利用して機能開発やサービス運用をしていきたい!という人を大募集してます。カジュアル面談も大歓迎ですのでご連絡ください。

Discussion