こんにちは。
C#で.NETアプリケーションを書いています。
テストメソッドを作ったときにそのメソッドへデータを渡す方法が2種類あります。
この2種類をテストケースの大きさとその視認性で使い分けるアイディアを紹介します。
DataRow形式
DataRow形式は、属性(Attribute)にテストケースを書くことでテストメソッドにデータを渡す方法です。
[DataTestMethod]
[DataRow("hoge", 100)]
[DataRow("huga", 10000)]
public void TestMethod(string hogeString, double hogeAmount)
{
// テスト内容がここにある
}
TestMethod()
のすぐ上に、[DataRow()]
という形でテストケースを書いています。
DynamicData形式
DynamicData形式は、テストケースを変数に入れておいて、その変数を属性で指定することでテストメソッドにデータを渡す方法です。
[DataTestMethod]
[DynamicData(nameof(HogeVariable))]
public void TestMethod(string hogeString, double hogeAmount)
{
// テスト内容がここにある
}
TestMethod()
のすぐ上に、[DynamicData(nameof())]
という形でHogeVariable
という変数を指定しています。
別途、HogeVariable
を以下のように用意します。
private static IEnumerable<object[]> HogeVariable => new List<object[]>()
{
// テストケース1
new object[]
{
hoge,
100
},
// テストケース2
new object[]
{
huga,
10000
}
};
HogeVariable
はobject[]
のリストとなっており、各object[]が1つのテストケースになっています。
実例と使い分け方
実例として、不動産の管理委託契約の消費税計算を取り上げます。
不動産の世界では、オーナーと管理会社との間で「管理委託契約」を結びます。
契約には契約金が付き物で、消費税の計算も必要となります。
そこで、契約金額(税抜)と消費税率から税込額を返す消費税計算メソッドCalcShohizei()
のテストコードを例とします。
契約金額(税抜)と消費税率は、KanriitakuKeiyaku
というクラスで持っているものとします。
DataRow形式の実例
[DataTestMethod]
[DataRow(100, 8, 108)]
[DataRow(100, 10, 110)]
public void CalcShohizeiTest1(double keiyakukinAmount, double shohizeiRitsu, double expectedZeikomiAmount)
{
var keiyaku = new KanriitakuKeiyaku()
{
keiyakukinAmount = keiyakukinAmount,
shohizeiRitsu = shohizeiRitsu
};
var actualZeikomiAmount = this.m_calculator.CalcShohizei(keiyaku);
Assert.AreEqual(expectedZeikomiAmount, actualZeikomiAmount);
}
[DataRow(100, 8, 108)]
という形で、テストメソッドのそれぞれの引数の値を指定しています。
この例ではDataRowが2行ありますが、テストケースを増やすときはDataRowを1行追加するだけです。
テストメソッドのすぐ上にDataRowがずらーっと並び、各行が1つのテストケースとなるため、テストケースを手っ取り早く確認できるようにしたいときにDataRow形式で書くと良いでしょう。
DynamicData形式の実例
[DataTestMethod]
[DynamicData(nameof(KanriitakuKeiyakuTestData))]
public void CalcShohizeiTest2(KanriitakuKeiyaku keiyaku, double expectedZeikomiAmount)
{
var actualZeikomiAmount = this.m_calculator.CalcShohizei(keiyaku);
Assert.AreEqual(expectedZeikomiAmount, actualZeikomiAmount);
}
private static IEnumerable<object[]> KanriitakuKeiyakuTestData => new List<object[]>()
{
// テストケース1
new object[]
{
new KanriitakuKeiyaku()
{
keiyakukinAmount = 100,
shohizeiRitsu = 8,
// .
// .
// .
// いろいろたくさん
},
108
},
// テストケース2
new object[]
{
new KanriitakuKeiyaku()
{
keiyakukinAmount = 100,
shohizeiRitsu = 10,
// .
// .
// .
// いろいろたくさん
},
110
}
};
KanriitakuKeiyakuTestData
という変数を用意し、[DynamicData(nameof(KanriitakuKeiyakuTestData))]
という形でテストデータを渡しています。
各テストケースはobject[]
型となっておりいろいろ入るので、各テストケースで用意するデータが多い場合や大きいクラスを渡す必要があるときにDynamicData形式で書くと良いでしょう。
まとめ
属性でテストケースも書いてしまうのがDataRow形式、変数を用意して属性では変数名を指定するのがDynamicData形式です。
DataRow形式もDynamicData形式も、テストケースが同じならテスト結果は同じですので、各テストケースの視認性とテストケースの大きさを天秤にかけて使い分けると良いのではないでしょうか。
今回は契約データを例に挙げましたが、実際の契約データでは契約金以外にも契約開始日・終了日などの値を持ち、これら日付等は税込金額算出時に使う消費税率の決定に関わってきます。
契約によっては、2019年10月1日以降も消費税率を10%とはしない「経過措置」が適用される、なんてこともあります。
これら諸々を考慮すると、テストケース1つ取っても契約金、契約開始日、...などたくさんの値の準備が必要で、DataRow形式だと1行がとても長くなるかやむなく複数行で書くことになり、視認性が悪いです。
こうなってくると、DynamicData形式で書くのが良いですね。
最後に
本記事では不動産の世界で用いられるKanriitakuKeiyaku
(管理委託契約)を例に取り上げました。
なぜ不動産の世界を例に挙げたかというと、本記事の著者は不動産テックの会社に所属しているからです。
株式会社いい生活の.NETアプリケーションチームでは、一緒に働くメンバーを募集しています!!
付録
皆様のお手元でもテストを実行できるように、テスト環境についてと書いたコードを置いておきます。
テスト環境
以下環境にて動かしました。
- OS: Windows 10 Pro 64bit version1809
- IDE: Visual Studio Professional 2019 version16.4.2
- プロジェクトの対象フレームワーク: .NET Core 3.1
テストフレームワークはMSTestを使用しました。
ソリューションを右クリック>ソリューションのNuGetパッケージの管理 からMSTest.TestFrameworkとMSTest.TestAdapterを入れると良いです。
テストを書いたら、テストエクスプローラーを表示して、テストクラスまたはメソッドを右クリック>実行 です。
テストコード
using System.Collections.Generic;
using csharp_testcode;
using Microsoft.VisualStudio.TestTools.UnitTesting; // https://github.com/microsoft/testfx
namespace csharp_testcode_test
{
[TestClass]
public class ShohizeiCalculatorTest
{
/// <summary>
/// 消費税計算クラス
/// </summary>
private Calculator m_calculator;
/// <summary>
/// テストの最初に実行される
/// </summary>
[TestInitialize]
public void Initialize()
{
this.m_calculator = new Calculator();
}
/// <summary>
/// 消費税計算メソッドのテスト
/// DataRow形式
/// </summary>
/// <param name="keiyakukinAmount">契約金本体額</param>
/// <param name="shohizeiRitsu">消費税率</param>
/// <param name="expectedZeikomiAmount">税込金額</param>
[DataTestMethod]
[DataRow(100, 8, 108)]
[DataRow(100, 10, 110)]
public void CalcShohizeiTest1(double keiyakukinAmount, double shohizeiRitsu, double expectedZeikomiAmount)
{
var keiyaku = new KanriitakuKeiyaku()
{
keiyakukinAmount = keiyakukinAmount,
shohizeiRitsu = shohizeiRitsu
};
var actualZeikomiAmount = this.m_calculator.CalcShohizei(keiyaku);
Assert.AreEqual(expectedZeikomiAmount, actualZeikomiAmount);
}
/// <summary>
/// 消費税計算メソッドのテスト
/// </summary>
/// <param name="keiyaku">管理委託契約</param>
/// <param name="expectedZeikomiAmount">税込金額</param>
[DataTestMethod]
[DynamicData(nameof(KanriitakuKeiyakuTestData))]
public void CalcShohizeiTest2(KanriitakuKeiyaku keiyaku, double expectedZeikomiAmount)
{
var actualZeikomiAmount = this.m_calculator.CalcShohizei(keiyaku);
Assert.AreEqual(expectedZeikomiAmount, actualZeikomiAmount);
}
/// <summary>
/// CalcShohizei()のテストデータ
/// </summary>
private static IEnumerable<object[]> KanriitakuKeiyakuTestData => new List<object[]>()
{
// テストケース1
new object[]
{
new KanriitakuKeiyaku()
{
keiyakukinAmount = 100,
shohizeiRitsu = 8,
// .
// .
// .
// いろいろたくさん
},
108
},
// テストケース2
new object[]
{
new KanriitakuKeiyaku()
{
keiyakukinAmount = 100,
shohizeiRitsu = 10,
// .
// .
// .
// いろいろたくさん
},
110
}
};
}
}
テストしたいクラス・メソッドのコード
namespace csharp_testcode
{
public class Calculator
{
/// <summary>
/// 本体額と消費税率から税込金額を計算する
/// </summary>
/// <param name="keiyaku"></param>
/// <returns></returns>
public double CalcShohizei(KanriitakuKeiyaku keiyaku)
{
var shohizeiAmount = keiyaku.keiyakukinAmount * keiyaku.shohizeiRitsu / 100;
return keiyaku.keiyakukinAmount + shohizeiAmount;
}
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace csharp_testcode
{
public class KanriitakuKeiyaku
{
private double m_keiyakukinAmount;
/// <summary>
/// 契約金本体額
/// </summary>
public double keiyakukinAmount
{
get => this.m_keiyakukinAmount;
set => this.m_keiyakukinAmount = value;
}
private double m_shohizeiRitsu;
/// <summary>
/// 消費税率
/// </summary>
public double shohizeiRitsu
{
get => this.m_shohizeiRitsu;
set => this.m_shohizeiRitsu = value;
}
}
}