このファクトリタイプのメソッドとデータベース呼び出しをテスト可能にリファクタリングするにはどうすればよいですか?
-
22-07-2019 - |
質問
ユニットテストとモッキングの方法を学ぼうとしています。 TDDと基本的なテストの原則のいくつかを理解しています。ただし、テストなしで記述された以下のコードのリファクタリングを検討しており、テスト可能にするためにどのように変更する必要があるかを理解しようとしています。
public class AgentRepository
{
public Agent Select(int agentId)
{
Agent tmp = null;
using (IDataReader agentInformation = GetAgentFromDatabase(agentId))
{
if (agentInformation.Read())
{
tmp = new Agent();
tmp.AgentId = int.Parse(agentInformation["AgentId"].ToString());
tmp.FirstName = agentInformation["FirstName"].ToString();
tmp.LastName = agentInformation["LastName"].ToString();
tmp.Address1 = agentInformation["Address1"].ToString();
tmp.Address2 = agentInformation["Address2"].ToString();
tmp.City = agentInformation["City"].ToString();
tmp.State = agentInformation["State"].ToString();
tmp.PostalCode = agentInformation["PostalCode"].ToString();
tmp.PhoneNumber = agentInformation["PhoneNumber"].ToString();
}
}
return tmp;
}
private IDataReader GetAgentFromDatabase(int agentId)
{
SqlCommand cmd = new SqlCommand("SelectAgentById");
cmd.CommandType = CommandType.StoredProcedure;
SqlDatabase sqlDb = new SqlDatabase("MyConnectionString");
sqlDb.AddInParameter(cmd, "AgentId", DbType.Int32, agentId);
return sqlDb.ExecuteReader(cmd);
}
}
これら2つのメソッドは単一のクラスにあります。 GetAgentFromDatabaseのデータベース関連コードは、エンタープライズライブラリに関連しています。
このテスト可能にする方法を教えてください。 GetAgentFromDatabaseメソッドを別のクラスに抽象化する必要がありますか? GetAgentFromDatabaseはIDataReader以外のものを返す必要がありますか?外部リンクへの提案やポインタは大歓迎です。
解決
GetAgentFromDatabase()を別のクラスに移動するのは正しいことです。 AgentRepository の再定義方法は次のとおりです。
public class AgentRepository {
private IAgentDataProvider m_provider;
public AgentRepository( IAgentDataProvider provider ) {
m_provider = provider;
}
public Agent GetAgent( int agentId ) {
Agent agent = null;
using( IDataReader agentDataReader = m_provider.GetAgent( agentId ) ) {
if( agentDataReader.Read() ) {
agent = new Agent();
// set agent properties later
}
}
return agent;
}
}
ここで、 IAgentDataProvider インターフェースを次のように定義しました:
public interface IAgentDataProvider {
IDataReader GetAgent( int agentId );
}
つまり、 AgentRepository はテスト対象のクラスです。 IAgentDataProvider をモックし、依存関係を注入します。 ( Moq で実行しましたが、別の分離フレームワークで簡単にやり直すことができます)。
[TestFixture]
public class AgentRepositoryTest {
private AgentRepository m_repo;
private Mock<IAgentDataProvider> m_mockProvider;
[SetUp]
public void CaseSetup() {
m_mockProvider = new Mock<IAgentDataProvider>();
m_repo = new AgentRepository( m_mockProvider.Object );
}
[TearDown]
public void CaseTeardown() {
m_mockProvider.Verify();
}
[Test]
public void AgentFactory_OnEmptyDataReader_ShouldReturnNull() {
m_mockProvider
.Setup( p => p.GetAgent( It.IsAny<int>() ) )
.Returns<int>( id => GetEmptyAgentDataReader() );
Agent agent = m_repo.GetAgent( 1 );
Assert.IsNull( agent );
}
[Test]
public void AgentFactory_OnNonemptyDataReader_ShouldReturnAgent_WithFieldsPopulated() {
m_mockProvider
.Setup( p => p.GetAgent( It.IsAny<int>() ) )
.Returns<int>( id => GetSampleNonEmptyAgentDataReader() );
Agent agent = m_repo.GetAgent( 1 );
Assert.IsNotNull( agent );
// verify more agent properties later
}
private IDataReader GetEmptyAgentDataReader() {
return new FakeAgentDataReader() { ... };
}
private IDataReader GetSampleNonEmptyAgentDataReader() {
return new FakeAgentDataReader() { ... };
}
}
( IDataReader を実装し、簡単なクラス FakeAgentDataReader の実装を省略しました- Read()のみを実装する必要がありますおよび Dispose()を使用してテストを機能させます。)
ここでの AgentRepository の目的は、 IDataReader オブジェクトを取得して、適切に形成された Agent オブジェクトに変換することです。上記のテストフィクスチャを展開して、より興味深いケースをテストできます。
実際のデータベースから分離して AgentRepository を単体テストした後、 IAgentDataProvider の具体的な実装のための単体テストが必要になりますが、これは別の質問のトピックです。 HTH
他のヒント
ここでの問題は、SUTとTestを決定することです。この例では、 Select()
メソッドをテストしようとしているため、それをデータベースから分離する必要があります。いくつかの選択肢があります。
-
GetAgentFromDatabase()
を仮想化して、正しい値を返すコードを派生クラスに提供できるようにします。この場合、IDataReaderFunctionaity
を提供するオブジェクトを作成します。 DBと通信するclass MyDerivedExample : YourUnnamedClass { protected override IDataReader GetAgentFromDatabase() { return new MyDataReader({"AgentId", "1"}, {"FirstName", "Fred"}, ...); } }
-
As Gishuは、IsA関係(継承)の代わりにHasa(オブジェクト構成)を使用することを提案しました。継承なしの時間。
ただし、これらの両方は、照会時に返される結果セットを単に定義する多くのコードになります。確かに、メインコードの代わりにこのコードをテストコードに保持することはできますが、それは努力です。あなたが本当にしているのは、特定のクエリの結果セットを定義することです。そして、あなたはそれを行うのに本当に良いことを知っています...データベース
-
しばらく前にLinqToSQLを使用しましたが、
DataContext
オブジェクトにはDeleteDatabase
やCreateDatabase
などの非常に便利なメソッドがあることがわかりました。public const string UnitTestConnection = "Data Source=.;Initial Catalog=MyAppUnitTest;Integrated Security=True"; [FixtureSetUp()] public void Setup() { OARsDataContext context = new MyAppDataContext(UnitTestConnection); if (context.DatabaseExists()) { Console.WriteLine("Removing exisitng test database"); context.DeleteDatabase(); } Console.WriteLine("Creating new test database"); context.CreateDatabase(); context.SubmitChanges(); }
しばらく検討してください。単体テストにデータベースを使用する際の問題は、データが変更されることです。データベースを削除し、テストを使用して、将来のテストで使用できるデータを進化させます。
注意すべき点が2つあります
テストが正しい順序で実行されることを確認してください。このためのMbUnit構文は [DependsOn(&quot; NameOfPreviousTest&quot;)]
です。
特定のデータベースに対して1セットのテストのみが実行されていることを確認してください。
いくつかのアイデアを掲載し始め、途中で更新します:
- SqlDatabase sqlDb = new SqlDatabase(&quot; MyConnectionString&quot;); -ロジックと混ざった new 演算子は避けてください。 xorを構築し、論理演算を使用する必要があります。それらが同時に起こるのを避けてください。依存性注入を使用してこのデータベースをパラメーターとして渡すと、モックできます。これは、単体テストを行う場合に意味します(データベースにはアクセスせず、後で行う必要があります)
- IDataReader agentInformation = GetAgentFromDatabase(agentId)-Readerの取得を他のクラスに分離できるため、ファクトリコードのテスト中にこのクラスをモックできます。
IMOは、通常、パブリックプロパティ/メソッドをテスト可能にすることのみを心配する必要があります。つまり Select(int agentId)が機能する限り、通常は GetAgentFromDatabase(int agentId)を介してどのように機能するかは気にしません。
あなたが持っているものは合理的と思われます。次のようなものでテストできると思います(クラスがAgentRepositoryと呼ばれると仮定します)
AgentRepository aRepo = new AgentRepository();
int agentId = 1;
Agent a = aRepo.Select(agentId);
//Check a here
推奨される機能強化について。 AgentRepositoryの接続文字列を、パブリックアクセスまたは内部アクセスによって変更できるようにすることをお勧めします。
クラス[NoName]のパブリックSelectメソッドをテストしようとしていると仮定します。
- GetAgentFromDatabase()メソッドをIDB_Accessなどのインターフェイスに移動します。 NoNameに、ctorパラメーターまたはプロパティとして設定できるインターフェイスメンバーを持たせます。これで継ぎ目ができたので、メソッドのコードを変更せずに動作を変更できます。
- 上記のメソッドの戻り値の型を変更して、より一般的なものを返します-ハッシュテーブルのように使用しているようです。 IDB_Accessのプロダクション実装でIDataReaderを使用して、ハッシュテーブルを内部的に作成します。また、技術への依存度が低くなります。 MySqlまたはMS / .net以外の環境を使用して、このインターフェイスを実装できます。
プライベートハッシュテーブルGetAgentFromDatabase(int agentId)
- 次に、単体テストでは、スタブを使用します(またはモックフレームワークのようなより高度なものを使用します)
。
public MockDB_Access : IDB_Access
{
public const string MY_NAME = "SomeName;
public Hashtable GetAgentFromDatabase(int agentId)
{ var hash = new Hashtable();
hash["FirstName"] = MY_NAME; // fill other properties as well
return hash;
}
}
// in the unit test
var testSubject = new NoName( new MockDB_Access() );
var agent = testSubject.Select(1);
Assert.AreEqual(MockDB_Access.MY_NAME, agent.FirstName); // and so on...
私の意見では、コードはSelect()メソッドのテストで完全にカバーされているため、GetAgentFromDatabase()メソッドは追加のテストでテストしてはいけません。コードが進むことのできるブランチはないため、ここで追加のテストを作成しても意味がありません。 ただし、GetAgentFromDatabase()メソッドが複数のメソッドから呼び出される場合、独自にテストする必要があります。