Come posso refactoring questo metodo di tipo di fabbrica e la chiamata al database per essere testabile?

StackOverflow https://stackoverflow.com/questions/1233486

Domanda

Sto cercando di imparare a fare Unit Testing e Mocking. Comprendo alcuni dei principi del TDD e dei test di base. Tuttavia, sto esaminando il refactoring del codice seguente che è stato scritto senza test e sto cercando di capire come deve essere modificato per renderlo testabile.

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);
}

}

Questi due metodi sono in una singola classe. Il codice relativo al database in GetAgentFromDatabase è correlato alle librerie Enterprise.

Come potrei fare per renderlo testabile? Devo astrarre il metodo GetAgentFromDatabase in una classe diversa? GetAgentFromDatabase dovrebbe restituire qualcosa di diverso da un IDataReader? Qualsiasi suggerimento o suggerimento a collegamenti esterni sarebbe molto apprezzato.

È stato utile?

Soluzione

Hai ragione a spostare GetAgentFromDatabase () in una classe separata. Ecco come ho ridefinito 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;
    }
}

dove ho definito l'interfaccia IAgentDataProvider come segue:

public interface IAgentDataProvider {
    IDataReader GetAgent( int agentId );
}

Quindi, AgentRepository è la classe sotto test. Derideremo IAgentDataProvider e inietteremo la dipendenza. (L'ho fatto con Moq , ma puoi facilmente rifarlo con un diverso framework di isolamento).

[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() { ... };
    }
}

(Ho tralasciato l'implementazione della classe FakeAgentDataReader , che implementa IDataReader ed è banale: devi solo implementare Leggi () e Dispose () per far funzionare i test.)

Lo scopo di AgentRepository qui è quello di prendere gli oggetti IDataReader e trasformarli in oggetti Agent correttamente formati. È possibile espandere il dispositivo di prova sopra per testare casi più interessanti.

Dopo il test unitario AgentRepository in isolamento dal database effettivo, saranno necessari test unitari per un'implementazione concreta di IAgentDataProvider , ma questo è un argomento per una domanda separata. HTH

Altri suggerimenti

Il problema qui è decidere cosa è SUT e cos'è Test. Con il tuo esempio stai provando a testare il metodo Select () e quindi vuoi isolarlo dal database. Hai diverse scelte,

  1. Virtualizza il GetAgentFromDatabase () in modo da poter fornire una classe derivata con codice per restituire i valori corretti, in questo caso creando un oggetto che fornisce IDataReaderFunctionaity senza parlando con il DB, cioè

    class MyDerivedExample : YourUnnamedClass
    {
        protected override IDataReader GetAgentFromDatabase()
        {
            return new MyDataReader({"AgentId", "1"}, {"FirstName", "Fred"},
              ...);
        }
    }
    
  2. As suggerito Gishu invece di usare le relazioni IsA (ereditarietà) usa HasA (composizione di oggetti) dove hai ancora una classe che gestisce la creazione di un IDataReader finto, ma questo tempo senza ereditare.

    Tuttavia, entrambi generano un sacco di codice che definisce semplicemente un insieme di risultati che ci verrà restituito quando richiesto. Certo, possiamo mantenere questo codice nel codice Test, anziché nel nostro codice principale, ma è uno sforzo. Tutto quello che stai veramente facendo è definire un set di risultati per query particolari e sai cosa è veramente bravo a farlo ... Un database

  3. Ho usato LinqToSQL qualche tempo fa e ho scoperto che gli oggetti DataContext hanno alcuni metodi molto utili, tra cui DeleteDatabase e 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();
    }
    

Consideralo per un po '. Il problema con l'utilizzo di un database per unit test è che i dati cambieranno. Elimina il tuo database e usa i tuoi test per evolvere i tuoi dati che possono essere utilizzati in test futuri.

Ci sono due cose da fare attenzione Assicurarsi che i test vengano eseguiti nell'ordine corretto. La sintassi MbUnit per questo è [DependsOn (" NameOfPreviousTest ")] . Assicurarsi che sia in esecuzione un solo set di test su un determinato database.

Inizierò a mettere alcune idee e aggiornerò lungo la strada:

  • SqlDatabase sqlDb = new SqlDatabase (" MyConnectionString "); - Dovresti evitare nuovi operatori confusi con la logica. Dovresti costruire xo avere operazioni logiche; evitare che accadano contemporaneamente. Utilizzare l'iniezione di dipendenza per passare questo database come parametro, in modo da poterlo deridere. Voglio dire questo se si desidera testarlo unitamente (non andare al database, che dovrebbe essere fatto in alcuni casi in seguito)
  • IDataReader agentInformation = GetAgentFromDatabase (agentId) - Forse potresti separare il recupero di Reader in un'altra classe, in modo da poter deridere questa classe durante il test del codice di fabbrica.

IMO dovresti normalmente preoccuparti solo di rendere testabili le tue proprietà / metodi pubblici. Cioè fintanto che Seleziona (int agentId) funziona normalmente non ti interessa come lo fa tramite GetAgentFromDatabase (int agentId) .

Quello che hai sembra ragionevole, poiché immagino che possa essere testato con qualcosa di simile al seguente (supponendo che la tua classe si chiami AgentRepository)

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

Per quanto riguarda i miglioramenti suggeriti. Consiglierei di consentire la modifica della stringa di connessione di AgentRepository, mediante accesso pubblico o interno.

Supponendo che tu stia provando a testare il metodo Select pubblico della classe [NoName] ..

  1. Sposta il metodo GetAgentFromDatabase () in un'interfaccia, ad esempio IDB_Access. Consenti a NoName di avere un membro di interfaccia che può essere impostato come parametro ctor o proprietà. Quindi ora hai una cucitura, puoi cambiare il comportamento senza modificare il codice nel metodo.
  2. Modificherei il tipo di ritorno del metodo sopra per restituire qualcosa di più generale: sembra che tu lo stia usando come un hashtable. Consenti all'implementazione di produzione di IDB_Access di utilizzare IDataReader per creare internamente l'hashtable. Inoltre lo rende meno dipendente dalla tecnologia; Posso implementare questa interfaccia usando MySql o un ambiente non MS / .net. Private Hashtable GetAgentFromDatabase (int agentId)
  3. Successivamente per il test unitario, potresti lavorare con uno stub (o utilizzare qualcosa di più avanzato come un framework simulato)

.

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...

Per quanto mi riguarda, il metodo GetAgentFromDatabase () non deve essere testato da un test aggiuntivo, poiché il suo codice è completamente coperto dal test del metodo Select (). Non ci sono rami che il codice potrebbe percorrere, quindi non ha senso creare un test extra qui. Se il metodo GetAgentFromDatabase () viene chiamato da più metodi, dovresti testarlo da solo.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top