12

単体テストとモッキングの方法を学ぼうとしています。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 つのメソッドは 1 つのクラスにあります。GetAgentFromDatabase のデータベース関連のコードは、エンタープライズ ライブラリに関連しています。

これをテスト可能にするにはどうすればよいですか?GetAgentFromDatabase メソッドを別のクラスに抽象化する必要がありますか? GetAgentFromDatabase は IDataReader 以外のものを返す必要がありますか? 外部リンクへの提案やポインタは大歓迎です。

4

6 に答える 6

9

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

于 2009-08-05T17:48:07.380 に答える
1

ここでの問題は、何が SUT で何がテストかを判断することです。あなたの例では、Select()メソッドをテストしようとしているため、それをデータベースから分離したいと考えています。いくつかの選択肢がありますが、

  1. を仮想化してGetAgentFromDatabase()、派生クラスに正しい値を返すコードを提供できるようにします。この場合IDataReaderFunctionaity、DB と対話せずに提供するオブジェクトを作成します。

    class MyDerivedExample : YourUnnamedClass
    {
        protected override IDataReader GetAgentFromDatabase()
        {
            return new MyDataReader({"AgentId", "1"}, {"FirstName", "Fred"},
              ...);
        }
    }
    
  2. IsAリレーションシップ (継承) を使用する代わりに Gishu が提案しIDataReaderたように、 HasA (オブジェクト構成) を使用します。この場合も、モックの作成を処理するクラスがありますが、今回は継承しません。

    ただし、これらは両方とも、クエリ時に返される一連の結果を定義するだけの多くのコードになります。確かに、このコードをメイン コードの代わりにテスト コードに保持することはできますが、それは手間がかかります。あなたが実際に行っているのは、特定のクエリの結果セットを定義することだけであり、それを行う上で何が本当に得意なのかを知っています... データベース

  3. しばらく前に LinqToSQL を使用したところ、オブジェクトにやDataContextなどの非常に便利なメソッドがあることがわかりました。DeleteDatabaseCreateDatabase

    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("NameOfPreviousTest")]. 特定のデータベースに対して 1 セットのテストのみが実行されていることを確認します。

于 2009-08-05T15:15:01.517 に答える
0

クラス [NoName] の public Select メソッドをテストしようとしているとします。

  1. GetAgentFromDatabase() メソッドを IDB_Access などのインターフェイスに移動します。NoName に、ctor パラメーターまたはプロパティとして設定できるインターフェイス メンバーを持たせます。これで継ぎ目ができたので、メソッドのコードを変更せずに動作を変更できます。
  2. 上記のメソッドの戻り値の型を変更して、より一般的なものを返すようにします-ハッシュテーブルのように使用しているようです。IDB_Access の実稼働実装で IDataReader を使用してハッシュテーブルを内部的に作成できるようにします。また、テクノロジーへの依存度も低くなります。このインターフェイスは、MySql または非 MS/.net 環境を使用して実装できます。 private Hashtable GetAgentFromDatabase(int agentId)
  3. 次の単体テストでは、スタブを使用できます (または、モック フレームワークのようなより高度なものを使用します)。

.

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...
于 2009-08-05T14:22:19.137 に答える
0

IMO 通常、パブリック プロパティ/メソッドをテスト可能にすることだけを心配する必要があります。つまり、 Select(int agentId)が機能する限り、通常はGetAgentFromDatabase(int agentId)を介してどのように機能するかは気にしません。

次のようなものでテストできると思うので、あなたが持っているものは合理的だと思われます(クラスがAgentRepositoryと呼ばれると仮定します)

AgentRepository aRepo = new AgentRepository();
int agentId = 1;
Agent a = aRepo.Select(agentId);
//Check a here

提案された拡張機能について。パブリック アクセスまたは内部アクセスのいずれかによって、AgentRepository の接続文字列を変更できるようにすることをお勧めします。

于 2009-08-05T14:21:43.117 に答える
0

私の意見では、GetAgentFromDatabase() メソッドは、Select() メソッドのテストによって完全にカバーされているため、追加のテストによってテストされるべきではありません。コードが進むことができる分岐がないため、ここで追加のテストを作成しても意味がありません。ただし、GetAgentFromDatabase() メソッドが複数のメソッドから呼び出される場合は、単独でテストする必要があります。

于 2009-08-05T14:34:00.417 に答える
0

私はいくつかのアイデアを出し始め、途中で更新します:

  • SqlDatabase sqlDb = new SqlDatabase("MyConnectionString"); -新しい演算子をロジックと混同しないようにする必要があります。xor have 論理演算を構成する必要があります。それらが同時に起こるのを避けてください。依存性注入を使用してこのデータベースをパラメーターとして渡し、モックできるようにします。単体テストをしたい場合はこれを意味します(データベースには行かず、後で行う必要があります)
  • IDataReader agentInformation = GetAgentFromDatabase(agentId) - Reader の取得を他のクラスに分離できるので、ファクトリ コードのテスト中にこのクラスをモックできます。
于 2009-08-05T14:11:39.667 に答える