Venomで実現するFIX APIテストの自動化

ogp

この記事はenechain Advent Calendar 2025の9日目の記事です。

1. イントロダクション

enechainでは、電力の卸取引プラットフォーム「eSquare Live」を開発しています。ユーザーはブラウザから電力の売買注文を行えるほか、金融取引向けに開発されたFIXプロトコルAPIを通じた注文も可能です。FIXプロトコルについては過去のブログ記事で詳しく解説していますので、併せてご参照ください。

私たちのシステムは以下の構成をとっています:

システムアーキテクチャ

  • FIXクライアント(取引先企業): FIXプロトコルに準拠して注文を送信する顧客側のシステム
  • FIXサーバー: FIXメッセージを受信し、gRPCリクエストに変換してgRPCサーバーへ転送
  • gRPCサーバー: ビジネスロジックを処理し、注文実行などを行うメインサーバー。実際はHTTPリクエストをgRPCに変換するプロキシーを経由してアクセスされる

FIXプロトコルは金融取引で広く使用される標準プロトコルですが、その自動テストには以下の課題があります:

  • 独自のメッセージフォーマット: タグ番号ベースの独自フォーマット(例: 35=D|55=SYMBOL)は可読性が低く、テスト作成やレスポンスのアサーションが困難
  • セッション管理: Logon/Logoutによるセッション確立・切断処理が必要で、セッション内ではコネクションが維持される
  • 非同期通信: FIXクライアントとサーバー間の通信が非同期のため、レスポンスの受信タイミングが不定
  • 既存ツールの非対応: 標準的なHTTPテストツールではFIXプロトコルを扱えない

これらの制限を克服するため、私たちはVenomを採用しました。

2. Venomとは - 既存ツールとの比較

2.1 Venomの概要

Venomは、フランスのクラウドプロバイダーOVHcloudが開発したオープンソースの統合テストフレームワークです。

主な特徴:

  • YAML形式の宣言的記述: テストシナリオを直感的に記述でき、非エンジニアでも理解可能
  • カスタムExecutorによる拡張性: 任意のプロトコルに対応可能な柔軟なアーキテクチャ
  • CI/CD統合: Jenkins、GitHub Actionsへの組み込みが容易

特に重要なのは、カスタムExecutorの実装により、FIXプロトコルのような特殊なプロトコルにも対応できる点です。これにより、FIXメッセージの送信から非同期レスポンスのアサーションまで、エンドツーエンドの自動テストを統一的なフレームワークで実現できました。

2.2 他ツールとの比較

FIX APIテストの実現方法として、私たちは複数のツールを検討しました:

Postman
世界で最も使われているAPIテストツールですが、HTTP/HTTPSに特化しており、TCP/IPソケット通信やFIXプロトコルには対応していません。GUIベースの直感的な操作が魅力ですが、私たちのユースケースには適用できませんでした。

runn
Go製のAPIテストツールで、YAMLベースのシンプルな記述が特徴です。しかし、HTTP、gRPC、SQLのサポートに留まり、カスタムプロトコル対応のためのプラグイン機構がないため、FIXプロトコルの追加実装が困難でした。

Scenarigo
Go製のE2Eテストフレームワークで、プラグインシステムによる拡張性を持ちます。理論的にはFIXプロトコル対応も可能ですが、VenomのカスタムExecutorと比較して実装が複雑で、学習コストと開発コストが高いと判断しました。

Venomの選択理由
最も重要な判断基準は、FIXプロトコルを簡単に扱えることでした。Venomはvenom.Executorインターフェースを実装するだけで、FIXプロトコルのような特殊なプロトコルに対応でき、既存のHTTP、SQL等と同じYAML記法でテストを記述できます。

他のツールでは、この要件を満たすために大きな開発コストが必要でしたが、Venomでは比較的短期間でFIX APIテストの自動化を実現できると判断しました。

3. Venomの基礎とカスタムExecutorの実装

3.1 標準機能の概要

Venomは以下の基本機能を提供しています:

  • YAMLベースの宣言的記述: テストシナリオをtestcasesstepsの階層構造で表現
  • 20種類以上のビルトインExecutor: HTTP、gRPC、SQL、Redis、Kafka、RabbitMQなどを標準サポート
  • 豊富なアサーション: ShouldEqual、ShouldContainSubstring、ShouldMatch(正規表現)など
  • 変数とデータ抽出: JSONPath、XPath、正規表現での値抽出と変数への格納

これらの機能により、一般的なWebAPIやデータベースのテストは追加実装なしで実現可能です。

3.2 カスタムExecutorによるFIXプロトコル対応 - 実装の詳細

前述のようにFIXプロトコルは金融取引に特化した以下の特性があり、標準Executorでは対応できません。ここではどのようにカスタムExecutorを実装して、VenomをFIXプロトコルに対応させたかを解説します。

カスタムExecutorの実装方法

Venomでは、venom.Executorインターフェースを実装するだけで、任意のプロトコルに対応できます。実際のFIX Executor実装を例に説明します。

1. Executorの構造体定義

package fix

type Executor struct {
    client    *client.Client  // FIXクライアント
    stopFuncs []func()        // セッション終了用関数
}

// テスト結果を格納する構造体
type Result struct {
    Messages []convert.Message `json:"messages,omitempty"`
}

2. venom.Executorインターフェースの実装

必要なのは主にRunメソッドの実装だけです:

func (e *Executor) Run(ctx context.Context, step venom.TestStep) (interface{}, error) {
    // YAMLから処理するFIXメッセージの種類(messageName)を取得
    msgName, _ := step.StringValue("messageName")
    
    switch msgName {
    case "logon":
        // FIXセッションの確立
        return nil, e.logon(ctx, step)
    case "logout":
        // セッションの終了
        e.logout()
        return nil, nil
    }
    
    // YAMLパラメータをFIXメッセージに変換
    msg, _ := convert.ConvertFromTestStep(step)
    fixMsg, _ := msg.Body.ToFIXMessage()

    // FIXリクエストを送信し、メッセージの受信を待機してrespsに格納する
    resps, _ = e.client.SendSync(ctx, fixMsg, expectedCount)
    
    // レスポンスを結果構造体に格納して返す
    result := Result{}
    for _, resp := range resps {
        // FIXメッセージを内部形式に変換して格納
    }
    return result, nil
}

3. Executorの登録

main関数でVenomに登録するだけで使用可能になります:

func main() {
    venom.RegisterExecutor(fix.Name, fix.New)
    venom.Process(testPath)
}

YAMLでのFIXテスト記述例

カスタムExecutor実装により、複雑なFIXプロトコルのテストもYAMLで簡潔に記述できます:

name: FIX API 統合テスト
testcases:
- name: 注文ライフサイクルテスト
  steps:
  # FIXセッション確立
  - type: fix
    messageName: Logon
    initiator:
      orderEntry:
        host: "fix-api.example.com"
        port: "9876"
      senderCompID: "TEST_CLIENT"
      targetCompID: "ENECHAIN"
      socketCertificateBase64: "{{.FIX_CERT}}"
      socketPrivateKeyBase64: "{{.FIX_KEY}}"
  
  # 新規注文の送信
  - type: fix
    messageName: NewOrderSingle
    body:
      ClOrdID: 'test-order-001'
      Side: "1"  # Buy
      Symbol: 'SYMBOL'
      OrderQty: 1000
      Price: 10.5
      OrdType: "2"  # Limit
    expectedMessageCount: 3
    assertions:
      # 注文受付確認
      - result.messages.__Len__ ShouldEqual 1
      - result.messages.messages0.body.ExecType ShouldEqual 0
      - result.messages.messages0.body.OrdStatus ShouldEqual 0
      - result.messages.messages0.body.Symbol ShouldEqual 'SYMBOL'
    vars:
      order-id:
        from: result.messages.messages0.body.OrderID
  
  # FIXセッション終了
  - type: fix
    messageName: Logout

カスタムExecutorがFIXテストの課題をどう解決したか

ここでは、冒頭で挙げたFIXプロトコルの課題を、カスタムExecutorの実装によってどのように解決したかを具体的に説明します。

独自メッセージフォーマットへの対応
FIXプロトコルはタグベースの独自フォーマット(例:35=D|55=SYMBOL|54=1|38=1000)を使用します。タグ番号の意味が直感的でないため可読性が低く、テスト作成が困難でした。この課題に対して、カスタムExecutorでは可読性の高いYAMLをFIXメッセージに変換するconvertorを実装しています。

まず、YAMLから受け取ったパラメータをGo構造体にマッピングし、その後FIXメッセージに変換します:

// テストケースのYAMLを読み取る処理
func ConvertFromTestStep(step venom.TestStep) (Message, error) {
    msgName, _ := step.StringValue("messageName")
    body, _ := step["body"]
    
    switch messageName(msgName) {
    case NewOrderSingle:
        var nos newordersingle.NewOrderSingle
        // YAMLのbodyをGo構造体にマッピング
        mapstructure.Decode(body, &nos)
        return Message{Type: NewOrderSingle, Body: nos}, nil
    // ...他のメッセージタイプ
    }
}

// YAMLから読み取った構造体をFIXメッセージへ変換する処理
func (n NewOrderSingle) ToFIXMessage() (*quickfix.Message, error) {
    msg := newordersingle.New(
        field.NewClOrdID(n.ClOrdID),
        field.NewSide(enum.Side(n.Side)),  // "1"→Buy
        field.NewTransactTime(time.Now()),
        field.NewOrdType(enum.OrdType_LIMIT),
    )
    msg.SetSymbol(n.Symbol)
    msg.SetOrderQty(decimal.NewFromFloat(n.OrderQty), 0)
    msg.SetPrice(decimal.NewFromFloat(n.Price), 2)
    return msg.Message, nil
}

ExecutorにFIXクライアントを保持させているので、Runメソッド内で上の処理を使って変換したFIXメッセージをクライアントから送信します:

type Executor struct {
    client *client.Client  // FIXクライアントを保持
}

func (e *Executor) Run(ctx context.Context, step venom.TestStep) (interface{}, error) {
    // YAMLパラメータをFIXメッセージに変換
    msg, _ := convert.ConvertFromTestStep(step)
    fixMsg, _ := msg.Body.ToFIXMessage()
    
    // FIXクライアントで送信
    resps, _ := e.orderEntryClient.SendSync(ctx, fixMsg, expectedCount)
}

この実装により、開発者はFIXの内部実装を知らなくても、前章のYAMLサンプルのようにSide: "1"Symbol: 'SYMBOL'といった直感的な記述でテストを作成できます。

セッション管理の自動化
FIXプロトコルでは、メッセージ交換の前にLogonメッセージでセッションを確立し、終了時にLogoutメッセージで切断する必要があります。さらに、金融システムではセキュリティのためmTLS(相互TLS)認証が必須となることが多く、これらの定型的な処理をExecutor内部に隠蔽しています。

messageName: "logon"を受け取ると、Executor内部で以下の処理を自動実行します:

func (e *Executor) logon(ctx context.Context, step venom.TestStep) error {
    var cfg InitiatorConfig
    mapstructure.Decode(step["initiator"], &cfg)
    
    // mTLS認証用の証明書と秘密鍵をBase64デコード
    privateKey, _ := base64.StdEncoding.DecodeString(cfg.SocketPrivateKeyBase64)
    certificate, _ := base64.StdEncoding.DecodeString(cfg.SocketCertificateBase64)
    
    // FIXセッション開始(内部でTLS接続・Logonメッセージ送信を実行)
    stopFunc, _ := initiator.Start(initiator.Config{
        Host: cfg.OrderEntry.Host,
        Port: cfg.OrderEntry.Port,
        SocketPrivateKeyBytes: string(privateKey),
        SocketCertificateBytes: string(certificate),
        SenderCompID: cfg.SenderCompID,
        TargetCompID: cfg.TargetCompID,
        // APIキーによる追加認証もここで設定
        ApiClientID: cfg.ApiClientID,
        ApiClientKey: cfg.ApiClientKey,
    })
    
    // クライアントインスタンスを生成してExecutorに保持
    e.client = client.New(cfg.SenderCompID, cfg.TargetCompID, ...)
    // 終了処理用の関数を保存(logout時に使用)
    e.stopFuncs = append(e.stopFuncs, stopFunc)
    return nil
}

セッション確立後はコネクションを維持し続ける必要がありますが、Executorにclientを保持させることでコネクションを維持しています。 こうすることでテスト実施の際にはmessageName: "Logon"messageName: "Logout"を記述するだけで、mTLS認証、コネクション管理などの複雑なFIXセッション管理から解放されます。

非同期レスポンスの同期的な検証
FIXプロトコルでは、送信したメッセージに対するレスポンスが非同期に返ってきます。また、1つの注文リクエストに対して「注文完了」「約定」といった複数のメッセージが期待値になることもあります。このような非同期通信を、Venomの同期的なテストフローに適合させるため、expectedMessageCountパラメータを導入しました。

SendSyncメソッドで、非同期メッセージを同期的に待機する仕組みを実装:

func (c *Client) SendSync(ctx context.Context, messageble quickfix.Messagable, msgCount int) ([]initiator.MessageFromApp, error) {
    // FIXメッセージを送信する前に、レスポンス待機用のgoroutineを起動
    var resp []initiator.MessageFromApp
    var wg sync.WaitGroup
    
    if msgCount > 0 {
        wg.Add(1)
        go func() {
            defer wg.Done()
            // 指定された数のメッセージを待機
            resp, err = c.WaitMessage(ctx, msgCount)
        }()
    }
    
    // FIXメッセージを送信
    quickfix.SendToTarget(msg, sessionID)
    
    // 全てのレスポンスを受信するまでブロック
    wg.Wait()
    return resp, nil
}

func (c *Client) WaitMessage(ctx context.Context, msgCount int) ([]initiator.MessageFromApp, error) {
    var msgs []initiator.MessageFromApp
    // 10秒のタイムアウトを設定
    ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
    defer cancel()
    
    for {
        select {
        case m := <-c.appMessageChan:  // 非同期に到着するメッセージをチャネルから受信
            msgs = append(msgs, m)
            if len(msgs) >= msgCount {  // 期待する数に達したら返す
                return msgs, nil
            }
        case <-ctx.Done():  // タイムアウト
            return nil, ctx.Err()
        }
    }
}

YAMLではexpectedMessageCountを指定して、非同期レスポンスを同期的に扱えます:

- type: fix
  messageName: NewOrderSingle
  body:
    ClOrdID: 'test-order-001'
    Side: "1"
  expectedMessageCount: 2  # 3つのレスポンス(注文完了・約定)を待つ
  assertions:
    - result.messages.__Len__ ShouldEqual 2

このように非同期通信を強制的に同期化することで、Venomでの逐次的なテスト実行を実現しています。

4. まとめ

私たちは、FIXプロトコルという特殊なプロトコルを使ったAPIのテストにVenomを採用しました。VenomのカスタムExecutorによる柔軟な拡張性のおかげで、複雑なFIX通信を簡潔なYAMLで記述できるようになりました。

本ツール導入前は、QAチームが手作業でFIXメッセージを組み立ててテストを実施していました。FIXメッセージの作成自体が煩雑な上、セッション確立やmTLS認証といった定型的な処理を毎回行う必要がありました。さらに、FIXレスポンスはタグベースの形式(35=8|39=0|150=0)で人間には読み解きにくく、アサーションにも時間を要していました。

カスタムExecutorの実装により、定型的なリグレッションテストを自動化し、テスト工数を大幅に削減できました。また、YAMLによる可読性の高いテスト記述により、FIXプロトコルの専門知識がなくてもテストケースの理解・修正が可能になり、チーム全体の生産性向上につながっています。


enechainでは、日本のエネルギー業界をテクノロジーの力で変革する仲間を募集しています。まずは気軽にカジュアル面談で話を聞いていただくだけで構いません。ご興味ある方はぜひTech Recruit Portalからご応募ください!

tech.enechain.com herp.careers