Como eu poderia refatorar este método do tipo fábrica e chamada de banco de dados para ser testável?

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

Pergunta

Eu estou tentando aprender a fazer o teste da unidade e zombaria. I compreender alguns dos princípios de TDD e testes básicos. No entanto, eu estou olhando para refatorar o código abaixo que foi escrito sem testes e estou tentando entender como ele precisa mudar, a fim de torná-lo testável.

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

}

Estes dois métodos estão em uma única classe. O código de banco de dados relacionadas na GetAgentFromDatabase está relacionada com a empresa Bibliotecas.

Como eu seria capaz de ir sobre como fazer essa testável? Devo abstrair o método GetAgentFromDatabase em uma classe diferente? Should GetAgentFromDatabase retorno algo diferente de um IDataReader? Todas as sugestões ou ponteiros para links externos seria muito apreciada.

Foi útil?

Solução

Você está correto sobre a movimentação GetAgentFromDatabase () em uma classe separada. Aqui está como eu redefiniu 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;
    }
}

onde eu definiu o IAgentDataProvider interface como segue:

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

Assim, AgentRepository é a classe em teste. Nós vamos zombar IAgentDataProvider e injetar a dependência. (Eu fiz isso com Moq , mas você pode facilmente refazê-lo com um quadro de isolamento diferente).

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

(eu deixei de fora a implementação de classe FakeAgentDataReader , que implementa IDataReader e é trivial - você só precisa implementar Read () e Dispose () para fazer a testes de trabalho.)

O objetivo do AgentRepository aqui é tomar IDataReader objetos e transformá-los em formada adequadamente Agent objetos. Igualmente é possível alargar a instalação de ensaio acima para testar casos mais interessantes.

Depois de testes de unidade AgentRepository isoladamente do banco de dados real, você vai precisar testes de unidade para uma implementação concreta de IAgentDataProvider , mas isso é assunto para uma questão em separado. HTH

Outras dicas

O problema aqui é decidir o que é SUT eo que é Test. Com seu exemplo você está tentando testar o método Select() e, portanto, queremos isolar que a partir do banco de dados. Você tem várias opções,

  1. virtualizar o GetAgentFromDatabase() para que possa fornecer uma classe derivada com código para retornar os valores corretos, neste caso, a criação de um objeto que fornece IDataReaderFunctionaity sem falar com a DB ou seja

    class MyDerivedExample : YourUnnamedClass
    {
        protected override IDataReader GetAgentFromDatabase()
        {
            return new MyDataReader({"AgentId", "1"}, {"FirstName", "Fred"},
              ...);
        }
    }
    
  2. Como Gishu sugeriu em vez de usar IsA relacionamentos (herança) uso Hasa (composição de objetos) onde, mais uma vez temos uma classe que lida com a criação de um IDataReader simulada, mas desta vez sem herdar.

    No entanto ambos resultado em lotes de código que simplesmente define um conjunto de resultados que ser devolvido quando consultado. É certo que nós podemos manter este código no código de teste, em vez de nosso código principal, mas seu esforço um. Tudo o que você está realmente fazendo é definir um conjunto de resultados para consultas particulares, e você sabe o que é realmente bom em fazer isso ... banco de dados A

  3. Eu costumava LinqToSQL um tempo atrás e descobriu que os objetos DataContext tem alguns métodos muito úteis, incluindo 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();
    }
    

Considere isso por um tempo. O problema com o uso de uma base de dados para testes de unidade é que os dados vai mudar. Excluir seu banco de dados e usar os testes para evoluir seus dados que podem ser usados ??em testes futuros.

Há duas coisas a ter cuidado Certifique-se de seus testes executados na ordem correta. A sintaxe MbUnit para isso é [DependsOn("NameOfPreviousTest")]. Certifique-se de apenas um conjunto de testes está correndo contra um banco de dados particular.

Eu vou começar a colocar algumas ideias e irá atualizar ao longo do caminho:

  • SqlDatabase SQLDB = new SqlDatabase ( "myConnectionString"); - Você deve evitar new operadores misturados com a lógica. Você deve construir xor tem operações lógicas; evitá-los acontecendo ao mesmo tempo. Usar injeção de dependência para passar esta base de dados como um parâmetro, para que você pode zombar dele. Quero dizer isso se você quiser teste de unidade (e não indo para o banco de dados, o que deve ser feito em alguns casos mais tarde)
  • IDataReader agentInformation = GetAgentFromDatabase (agentId) -. Talvez você pudesse separar a recuperação Reader para alguma outra classe, para que você pode zombar esta classe ao testar o código de fábrica

IMO você deve normalmente só se preocupar em fazer suas propriedades públicas / métodos testável. Ou seja, enquanto Select (int agentId) funciona normalmente você não se importa como ele faz isso através de GetAgentFromDatabase (int agentId) .

O que você tem parece razoável, como eu imagino que pode ser testado com algo como o seguinte (assumindo que sua classe é chamado AgentRepository)

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

Quanto a melhorias sugeridas. Eu recomendaria permitindo seqüência de conexão do AgentRepository de ser mudado, quer por acesso público ou interno.

Assumindo que você está tentando testar o método Select público de classe [NoName] ..

  1. Mover o método GetAgentFromDatabase () em um IDB_Access interface de dizer. Let NoName tem um membro de interface que pode ser definido como um parâmetro ctor ou uma propriedade. Então agora você tem uma costura, você pode alterar o comportamento sem modificar o código no método.
  2. Eu mudaria o tipo de retorno do método acima para retornar algo mais geral - você parece estar usando-o como um hashtable. Deixe a implementação de produção de IDB_Access usar o IDataReader para criar o hashtable internamente. Ele também faz com que seja menos dependente da tecnologia; Eu pode implementar esta interface usando MySQL ou algum ambiente não-MS / NET. private Hashtable GetAgentFromDatabase(int agentId)
  3. Avançar para o seu teste de unidade, você pode trabalhar com um topo (ou o uso de algo mais avançado, como um quadro simulado)

.

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

Quanto a minha opinião o método GetAgentFromDatabase () não deve ser testet por um teste extra, porque seu código é totalmente coberto pelo teste do método Select (). Não há ramos do código poderia caminhar ao longo, de modo que nenhum ponto na criação de um teste extra aqui. Se o método GetAgentFromDatabase () é chamado a partir de vários métodos que você deve testá-lo em seu próprio embora.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top