Comment pourrais-je refactoriser cette méthode de type factory et cet appel de base de données pour pouvoir être testé?

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

Question

J'essaie d'apprendre à faire des tests unitaires et des moqueries. Je comprends certains des principes du TDD et des tests de base. Cependant, je cherche à refactoriser le code ci-dessous qui a été écrit sans tests et essaie de comprendre comment il doit changer pour le rendre testable.

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

}

Ces deux méthodes sont dans une seule classe. Le code associé à la base de données dans GetAgentFromDatabase est lié aux bibliothèques d'entreprise.

Comment pourrais-je faire pour que cela soit testable? Dois-je résumer la méthode GetAgentFromDatabase dans une classe différente? GetAgentFromDatabase doit-il renvoyer autre chose qu'un IDataReader? Toute suggestion ou pointeur vers des liens externes serait grandement apprécié.

Était-ce utile?

La solution

Vous avez raison de déplacer GetAgentFromDatabase () dans une classe distincte. Voici comment j'ai redéfini 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;
    }
}

où j'ai défini l'interface IAgentDataProvider comme suit:

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

Ainsi, AgentRepository est la classe à tester. Nous nous moquerons de IAgentDataProvider et injecterons la dépendance. (Je l’ai fait avec Moq , mais vous pouvez facilement le refaire avec un cadre d’isolation différent).

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

(J'ai oublié l'implémentation de la classe FakeAgentDataReader , qui implémente IDataReader et qui est simple: vous devez uniquement implémenter Read () et Dispose () pour que les tests fonctionnent.)

AgentRepository a pour objet de récupérer les objets IDataReader et de les transformer en objets Agent correctement formés. Vous pouvez développer le dispositif de test ci-dessus pour tester des cas plus intéressants.

Après avoir testé AgentRepository dans l'unité, vous aurez besoin de tests unitaires pour une implémentation concrète de IAgentDataProvider , mais le sujet de cette question est distinct. HTH

Autres conseils

Le problème ici est de décider ce qui est SUT et ce qui est Test. Avec votre exemple, vous essayez de tester la méthode Select () et vous souhaitez donc l'isoler de la base de données. Vous avez plusieurs choix,

  1. Virtualisez le GetAgentFromDatabase () afin de pouvoir fournir à une classe dérivée un code lui permettant de retourner les valeurs correctes. Dans ce cas, créez un objet fournissant IDataReaderFunctionaity sans parler à la DB c'est-à-dire

    class MyDerivedExample : YourUnnamedClass
    {
        protected override IDataReader GetAgentFromDatabase()
        {
            return new MyDataReader({"AgentId", "1"}, {"FirstName", "Fred"},
              ...);
        }
    }
    
  2. As Gishu a suggéré au lieu d'utiliser des relations IsA (héritage), utilisez HasA (composition d'objet) dans lequel vous avez à nouveau une classe qui gère la création d'un IDataReader , mais cette temps sans hériter.

    Cependant, ces deux éléments génèrent beaucoup de code qui définit simplement un ensemble de résultats qui nous seront renvoyés lorsque nous les interrogerons. Certes, nous pouvons conserver ce code dans le code de test, au lieu de notre code principal, mais c’est un effort. Tout ce que vous faites réellement est de définir un ensemble de résultats pour des requêtes particulières, et vous savez ce qui est vraiment bien pour le faire ... Une base de données

  3. J'ai utilisé LinqToSQL il y a quelque temps et j'ai découvert que les objets DataContext avaient des méthodes très utiles, notamment DeleteDatabase et 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();
    }
    

Considérez cela un moment. Le problème avec l'utilisation d'une base de données pour les tests unitaires est que les données vont changer. Supprimez votre base de données et utilisez vos tests pour faire évoluer vos données et les utiliser lors de futurs tests.

Il y a deux choses à faire attention Assurez-vous que vos tests se déroulent dans le bon ordre. La syntaxe de MbUnit pour cela est [DependsOn (& Name; NameOfPreviousTest ")) . Assurez-vous qu’un seul ensemble de tests est en cours d’exécution sur une base de données particulière.

Je vais commencer à mettre en place quelques idées et à mettre à jour en cours de route:

  • SqlDatabase sqlDb = new SqlDatabase (" MyConnectionString "); - Vous devez éviter les nouveaux opérateurs mêlés à la logique. Vous devriez construire xor avoir des opérations logiques; évitez qu'ils se produisent en même temps. Utilisez l'injection de dépendance pour transmettre cette base de données en tant que paramètre afin de pouvoir vous moquer de celle-ci. Je veux dire cela si vous voulez le tester à l'unité (ne pas aller à la base de données, ce qui devrait être fait ultérieurement)
  • IDataReader agentInformation = GetAgentFromDatabase (agentId) - Vous pouvez peut-être séparer la récupération de Reader dans une autre classe afin de pouvoir vous moquer de cette classe lors du test du code d'usine.

IMO, vous ne devriez normalement vous soucier que de rendre testables vos propriétés / méthodes publiques. C'est à dire. tant que Select (int agentId) fonctionne, vous ne vous en souciez pas normalement via GetAgentFromDatabase (int agentId) .

Ce que vous avez semble raisonnable, car j'imagine qu'il peut être testé avec quelque chose comme ce qui suit (en supposant que votre classe s'appelle AgentRepository)

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

En ce qui concerne les améliorations suggérées. Je recommanderais d'autoriser la modification de la chaîne de connexion de AgentRepository, par accès public ou interne.

En supposant que vous essayez de tester la méthode de sélection publique de la classe [NoName] ..

  1. Déplacez la méthode GetAgentFromDatabase () dans une interface, par exemple IDB_Access. Laissez NoName avoir un membre d'interface qui peut être défini en tant que paramètre ctor ou propriété. Alors maintenant que vous avez une couture, vous pouvez changer le comportement sans modifier le code dans la méthode.
  2. Je changerais le type de retour de la méthode ci-dessus pour renvoyer quelque chose de plus général - vous semblez l'utiliser comme une table de hachage. Laissez l'implémentation de production d'IDB_Access utiliser IDataReader pour créer la table de hachage en interne. Cela le rend également moins dépendant de la technologie; Je peux implémenter cette interface en utilisant MySql ou un environnement non-MS / .net. Table de hachage privée GetAgentFromDatabase (int agentId)
  3. Ensuite, pour votre test unitaire, vous pouvez travailler avec un talon (ou utiliser quelque chose de plus avancé, comme un framework factice)

.

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

Pour moi, la méthode GetAgentFromDatabase () ne doit pas être testée par un test supplémentaire, car son code est entièrement couvert par le test de la méthode Select (). Il n'y a pas de branches dans lesquelles le code pourrait marcher, il est donc inutile de créer ici un test supplémentaire. Si la méthode GetAgentFromDatabase () est appelée à partir de plusieurs méthodes, vous devez l’essayer seule.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top