OpenTelemrationを使用して統合テストを改善する方法


この記事はHello Worldシリーズの一部です.そこで、我々はあなたのためにマイクロサービス関連の話題に取り組みます.私たちのチームは、一般的な問題のためのWebを検索し、我々は自分自身を解決し、完全なハウツーガイドをもたらす.aspectoは分散アプリケーションの開発者とチームのためのOpenTelemrationベースの分散追跡プラットフォームです.
注:このチュートリアルでは、OpenTementStation、トレース、およびスパンに精通していると仮定します.場合は、OpenTelemrationについての詳細を確認し、チェックアウトします.これは無料で、ベンダー中立、6つのエピソードのビデオシリーズは、あなたがOpenFeelTerrent、ゼロからヒーローに始めるために知っておく必要があるすべてをもたらします.

導入
の進化OpenTelemetry (OTEL) 近年、彼らのマイクロサービスをよりよく理解することに興味を持っている開発者にとって、彼らのサービスを計って、望ましい眺めを得ることは、より簡単になりました.
しかし、これまでのところ、主に生産問題のデバッグに使用されています.
私はあなたの統合テスト環境で使用することによって、生産問題を防ぐためにOpenTelemrationの力を利用する方法があると言ったらどうですか?
面白いね?読んで私はそれが簡単に行うことができますどのように表示します.

それは統合テストでOpenTelemrationを使用するように見えるか
最後の目標は、テスト中にテスト中に我々のサービスを計器し、作成されたスパン上でアサーションを行うことです.
これはトレースベースのテストです.
今、あなたはそのような実装の上で実質的に考えるようになっていて、「ああ、それで、私は現在テストテストでopentelemration SDKを統合する必要がありますか?」
さて、それらの質問は本当に合法的です.
幸いにも、あなた自身のすべてを実装する必要はありません.
このユースケースは正確に何の創造につながっているMalabi , OpenTelemration SDKをラップし、あなたのプロジェクトに追加することができますし、アサートを開始するように、すべてのこのセットアップを行うオープンソース.
P . S .は、トレースベースのテストとmalabiの概念についての詳細を読むことができます.


どのように、マラウイは助けますか?
(実用ガイド下、理論部分)
それが動作する方法は簡単です.
NPMやヤーンを使用してプロジェクトにMalabiを追加し、サービスのメインファイルの先頭に3行のコードを追加します.
次に、MalabiはOpenTelemration SDKを使用してサービスを実行します(統合テストのコンテキストで-例えばCIで)実行されます.
Malabiはこれらのスパンをメモリに格納し、これらのスパンにアクセスできるエンドポイントを公開し、あなたのアサーションに必要なデータを抽出するユーティリティ関数を提供します.

実用的な部分-どのように既存のNodeJS MicroServiceを取るし、統合テストでアサーションを作成するOpenTelemrationを利用する
次のコードは、テストしたいマイクロサービスのExpressJjsコードです.
これは、SQLiteを使用してメモリ内のデータベースとして保存し、ユーザーに関するデータを取得するデータベースです.また、高速検索のためのREDISキャッシュ内のフェッチされたデータの一部を格納します.
ここにインデックスのコードがあります.TSファイル:

第1部-マイクロサービスコード
注:あなたは、マラウイで完全なコードを見つけることができますexamples フォルダ
インデックス.jsファイル
import * as malabi from 'malabi';
malabi.instrument();
malabi.serveMalabiFromHttpApp(18393);

import axios from 'axios';
import express from 'express';
import body from "body-parser";
import User from "./db";
import { getRedis } from "./redis";
import Redis from "ioredis";
let redis: Redis.Redis;

getRedis().then((redisConn) => {
   redis = redisConn;
   app.listen(PORT, () => console.log(`service-under-test started at port ${PORT}`));

})
const PORT = process.env.PORT || 8080;

const app = express();
app.use(body.json())
app.get('/',(req,res)=>{
   res.sendStatus(200);
})
app.get('/todo', async (req, res) => {
   try {
       const todoItem = await axios('https://jsonplaceholder.typicode.com/todos/1');
       res.json({
           title: todoItem.data.title,
       });
   } catch (e) {
       res.sendStatus(500);
       console.error(e, e);
   }
});

app.get('/users', async (req, res) => {
   try {
       const users = await User.findAll({});
       res.json(users);
   } catch (e) {
       res.sendStatus(500);
       console.error(e, e);
   }
});

app.get('/users/:firstName', async (req, res) => {
   try {
       const firstName = req.param('firstName');
       if (!firstName) {
           res.status(400).json({ message: 'Missing firstName in url' });
           return;
       }

       let users = [];
       users = await redis.lrange(firstName, 0, -1);
       if (users.length === 0) {
           users = await User.findAll({ where: { firstName } });
           if (users.length !== 0) {
               await redis.lpush(firstName, users)
           }
       }

       res.json(users);
   } catch (e) {
       res.sendStatus(500);
       console.error(e, e);
   }
});

app.post('/users', async (req, res) => {
   try {
       const { firstName, lastName } = req.body;
       const user = await User.create({ firstName, lastName });
       res.json(user);
   } catch (e) {
       res.sendStatus(500);
   }
})
上記のファイルでは、マイクロサービスのすべてのエンドポイントを参照してください.ほとんどは自己説明的です-キャッシュとしてSQLite DB/REDISでデータを保存して、フェッチしてください.
しかし、Malabi魔法が起こるトップ3行に注意してください.
import * as malabi from 'malabi';
malabi.instrument();
malabi.serveMalabiFromHttpApp(18393);
基本的に、私たちはMalabiを必要とします(NPMのインストールを実行した後に-もちろん、devのmalabiを保存してください).
その後、マラビーは私たちのサービスを計る-それはそれが実行するようにスパン(メモリ上)を作成する意味.
その時点で、我々はポート18393から作成されたスパンを提供するように指示します.
Part 2では、このエンドポイントを問い合わせるためにどのようにMalabi Util関数を使用しているかを参照してください.しかし、まず、我々のサービスを理解し続けましょう.
このDBSQLiteを順番に処理するTSファイル:
import { Sequelize, DataTypes } from 'sequelize';

const sequelize = new Sequelize({
   dialect: 'sqlite',
   storage: ':memory:'
});

const User = sequelize.define('User', {
   firstName: {
       type: DataTypes.STRING,
       allowNull: false
   },
   lastName: {
       type: DataTypes.STRING
   }
});

User.sync({ force: true }).then(() => {
   User.create({ firstName: "Rick", lastName: 'Sanchez' });
})

export default User;
レッドTSファイル:
import { RedisMemoryServer } from 'redis-memory-server';
import Redis from "ioredis";
const redisServer = new RedisMemoryServer();

export async function getRedis() {
   const host = await redisServer.getHost();
   const port = await redisServer.getPort();
   const redis = new Redis(port, host);
   return redis;
}

パート2 -テストコード

テスト中のサービスspects . tsファイル:
このファイルはJestを使用して実行されます.
サービス自体のポートを持っていることに注意してください.
また、Malabiユーティリティ関数もあります.fetchRemoteTelemetry , clearRemoteTelemetry 彼らの名前のように、アサーションのために終点からスパンを取って、メモリ内キャッシュをクリアしてください(毎回、きれいなスレートを維持するためにテストの間できれいにするのに役に立ちます).
以下のコードを見てください.
const SERVICE_UNDER_TEST_PORT = process.env.PORT || 8080;
import axios from 'axios';
import { fetchRemoteTelemetry, clearRemoteTelemetry } from 'malabi';
const getTelemetryRepository = async () => await fetchRemoteTelemetry({ portOrBaseUrl: 18393 });

describe('testing service-under-test remotely', () => {
   beforeEach(async () => {
       // We must reset all collected spans between tests to make sure spans aren't leaking between tests.
       await clearRemoteTelemetry({ portOrBaseUrl: 18393 });
   });

   it('successful /todo request', async () => {
       // call to the service under test - internally it will call another API to fetch the todo items.
       const res = await axios(`http://localhost:${SERVICE_UNDER_TEST_PORT}/todo`);

       // get spans created from the previous call
       const telemetryRepo = await getTelemetryRepository();

       // Validate internal HTTP call
       const todoInteralHTTPCall = telemetryRepo.spans.outgoing().first;
       expect(todoInteralHTTPCall.httpFullUrl).toBe('https://jsonplaceholder.typicode.com/todos/1')
       expect(todoInteralHTTPCall.statusCode).toBe(200);
   });

   it('successful /users request', async () => {
       // call the service under test
       const res = await axios.get(`http://localhost:${SERVICE_UNDER_TEST_PORT}/users`);

       // get spans created from the previous call
       const telemetryRepo = await getTelemetryRepository();

       // Validating that /users had ran a single select statement and responded with an array.
       const sequelizeActivities = telemetryRepo.spans.sequelize();
       expect(sequelizeActivities.length).toBe(1);
       expect(sequelizeActivities.first.dbOperation).toBe("SELECT");
       expect(Array.isArray(JSON.parse(sequelizeActivities.first.dbResponse))).toBe(true);
   });

   it('successful /users/Rick request', async () => {
       // call the service under test
       const res = await axios.get(`http://localhost:${SERVICE_UNDER_TEST_PORT}/users/Rick`);

       // get spans created from the previous call
       const telemetryRepo = await getTelemetryRepository();

       const sequelizeActivities = telemetryRepo.spans.sequelize();
       expect(sequelizeActivities.length).toBe(1);
       expect(sequelizeActivities.first.dbOperation).toBe("SELECT");

       const dbResponse = JSON.parse(sequelizeActivities.first.dbResponse);
       expect(Array.isArray(dbResponse)).toBe(true);
       expect(dbResponse.length).toBe(1);
   });

   it('Non existing user - /users/Rick111 request', async () => {
       // call the service under test
       const res = await axios.get(`http://localhost:${SERVICE_UNDER_TEST_PORT}/users/Rick111`);

       // get spans created from the previous call
       const telemetryRepo = await getTelemetryRepository();

       const sequelizeActivities =  telemetryRepo.spans.sequelize();
       expect(sequelizeActivities.length).toBe(1);
       expect(sequelizeActivities.first.dbOperation).toBe("SELECT");

       const dbResponse = JSON.parse(sequelizeActivities.first.dbResponse);
       expect(Array.isArray(dbResponse)).toBe(true);
       expect(dbResponse.length).toBe(0);

       expect(telemetryRepo.spans.httpGet().first.statusCode).toBe(200);
   });

   it('successful POST /users request', async () => {
       // call the service under test
       const res = await axios.post(`http://localhost:${SERVICE_UNDER_TEST_PORT}/users`,{
           firstName:'Morty',
           lastName:'Smith',
       });

       expect(res.status).toBe(200);

       // get spans created from the previous call
       const telemetryRepo = await getTelemetryRepository();

       // Validating that /users created a new record in DB
       const sequelizeActivities =  telemetryRepo.spans.sequelize();
       expect(sequelizeActivities.length).toBe(1);
       expect(sequelizeActivities.first.dbOperation).toBe("INSERT");
   });


   /* The expected flow is:
       1) Insert into db the new user (due to first API call; POST /users).
       ------------------------------------------------------------------
       2) Try to fetch the user from Redis (due to the second API call; GET /users/Jerry).
       3) The user shouldn't be present in Redis so fetch from DB.
       4) Push the user object from DB to Redis.
   */
   it('successful create and fetch user', async () => {
       // Creating a new user
       const createUserResponse = await axios.post(`http://localhost:${SERVICE_UNDER_TEST_PORT}/users`,{
           firstName:'Jerry',
           lastName:'Smith',
       });
       expect(createUserResponse.status).toBe(200);

       // Fetching the user we just created
       const fetchUserResponse = await axios.get(`http://localhost:${SERVICE_UNDER_TEST_PORT}/users/Jerry`);
       expect(fetchUserResponse.status).toBe(200);

       // get spans created from the previous calls
       const telemetryRepo = await getTelemetryRepository();
       const sequelizeActivities = telemetryRepo.spans.sequelize();
       const redisActivities =  telemetryRepo.spans.redis();

       // 1) Insert into db the new user (due to first API call; POST /users).
       expect(sequelizeActivities.first.dbOperation).toBe('INSERT');
       // 2) Try to fetch the user from Redis (due to a second API call; GET /users/Jerry).
       expect(redisActivities.first.dbStatement).toBe("lrange Jerry 0 -1");
       expect(redisActivities.first.dbResponse).toBe("[]");
       // 3) The user shouldn't be present in Redis so fetch from DB.
       expect(sequelizeActivities.second.dbOperation).toBe("SELECT");
       //4) Push the user object from DB to Redis.
       expect(redisActivities.second.dbStatement.startsWith('lpush Jerry')).toBeTruthy();
   });
});
一旦我々がスパンを取ったならば、我々はトレースベースのテストに関係なく、我々が他のテストでそうするように、主張をするために、Jestの期待関数を使用することができます.

例1 -「成功/ユーザ要求」という名前のテストを実行します.
例えば、「成功/ユーザー要求」という名前のテストなどのコードを調べましょう.
まず、すべてのユーザを取得するサービスを呼び出します.
では、fetchRemoteTelemetry ラップgetTelemetryRepository 関数は、マラウイからスパンを取得します.
その後、Sequelize Accessorを使用して、シーケンサのスパンのみをフィルターします.
一旦我々が手でシーケンシャル・スパンを持つならば、我々が1回だけDBを得るとき、我々は1を持つと主張することができます.
我々はまた、それが選択操作であることを知っているので、我々はそれが選択操作だと主張する.

例2 -「成功したCREATE AND FETCH User」という名前のテストを行います.
もう少し複雑なテストを調べましょう.
指定されたテストでは、ポスト/ユーザーを使用して新しいユーザーを作成します.それから、get/user/: firstnameを使ってそのユーザに問い合わせを試みます.
予想通り、我々はあなたが他のテストで200として主張する.
ここで再び、関連するスパンを取得するためにMalabiユーティリティを使用し、変数に格納します.
const telemetryRepo = await getTelemetryRepository();
const sequelizeActivities = telemetryRepo.spans.sequelize();
const redisActivities =  telemetryRepo.spans.redis();
最初のアサーション-最初のPOST操作がDB INSERT操作を引き起こしたことを確認します.
expect(sequelizeActivities.first.dbOperation).toBe('INSERT');
ユーザが作成されたので、REDISでは存在しないことを期待していますので、REDISクエリをRESEISから空の配列にすることを期待します.
expect(redisActivities.first.dbStatement).toBe("lrange Jerry 0 -1");
expect(redisActivities.first.dbResponse).toBe("[]");
ユーザーがREDISに存在しなかったので、我々はDBをフェッチしたことを予期します
expect(sequelizeActivities.second.dbOperation).toBe("SELECT");
そして今、REDISは実際の生活を確実にするためにpushコマンドを受け取ったことを期待しています(全てをきれいにしてからテストを実行しないでください).
expect(redisActivities.second.dbStatement.startsWith('lpush Jerry')).toBeTruthy();
そうでしょう私はあなたがどのように簡単にそれがはるかに簡単な方法で前に強力な統合テストを書くためにOpenTelemration&Malabiを使用することができるかを見ることができます.
P . S . Malabiは新しいアプローチを実装している比較的新しいライブラリです、そして、その著者(私自身含まれる)はそれの上であなたの考えを聞いて、どんな改善/提案をあなたが聞くのが好きでしょう.だから自由に開くdiscussion githubで、またはDMS経由で私に連絡してください.