Как я могу реорганизовать этот фабричный метод и вызов базы данных, чтобы их можно было тестировать?

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

Вопрос

Я пытаюсь научиться проводить модульное тестирование и издевательство.Я понимаю некоторые принципы 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);
}

}

Эти два метода находятся в одном классе.Код, связанный с базой данных, в 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() { ... };
    }
}

(я упустил реализацию класса FakeAgentDataReader , который реализует IDataReader и является тривиальным - вам нужно только реализовать Read () и Dispose () , чтобы тесты работали.)

Цель AgentRepository здесь состоит в том, чтобы взять объекты IDataReader и превратить их в правильно сформированные объекты Agent . Вы можете расширить вышеуказанное тестовое устройство, чтобы проверить более интересные случаи.

После модульного тестирования AgentRepository в отрыве от реальной базы данных вам понадобятся модульные тесты для конкретной реализации IAgentDataProvider , но это тема отдельного вопроса. НТН

Другие советы

Проблема здесь в том, чтобы решить, что такое SUT и что такое Test. В вашем примере вы пытаетесь протестировать метод Select () и поэтому хотите изолировать его от базы данных. У вас есть несколько вариантов,

<Ол>
  • Виртуализируйте GetAgentFromDatabase () , чтобы вы могли предоставить производному классу код для возврата правильных значений, в этом случае создайте объект, который предоставляет IDataReaderFunctionaity без разговаривать с БД т.е.

    class MyDerivedExample : YourUnnamedClass
    {
        protected override IDataReader GetAgentFromDatabase()
        {
            return new MyDataReader({"AgentId", "1"}, {"FirstName", "Fred"},
              ...);
        }
    }
    
  • As Гишу предложил вместо использования отношений IsA (наследование) использовать HasA (составление объектов), где у вас снова есть класс, который обрабатывает создание фиктивного IDataReader , но это время без наследования.

    Однако оба эти результата приводят к большому количеству кода, который просто определяет набор результатов, которые мы возвращаем при запросе. По общему признанию мы можем сохранить этот код в тестовом коде вместо нашего основного кода, но это усилие. Все, что вы на самом деле делаете, это определяете набор результатов для конкретных запросов, и вы знаете, что действительно хорошо для этого делается ... База данных

  • Некоторое время назад я использовал 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();
    }
    
  • Обдумайте это на некоторое время. Проблема с использованием базы данных для модульных тестов заключается в том, что данные будут меняться. Удалите свою базу данных и используйте свои тесты для развития ваших данных, которые могут быть использованы в будущих тестах.

    Есть две вещи, о которых нужно быть осторожными Убедитесь, что ваши тесты выполняются в правильном порядке. Синтаксис MbUnit для этого: [DependsOn (" NameOfPreviousTest ")] . Убедитесь, что с определенной базой данных работает только один набор тестов.

    Я начну высказывать некоторые идеи и буду обновлять по ходу дела:

    • SqlDatabase sqlDb = новая SqlDatabase("MyConnectionString");- Вам следует избегать новый операторы перепутались с логикой.Вы должны построить xor с логическими операциями;избегайте того, чтобы они происходили одновременно.Используйте внедрение зависимостей, чтобы передать эту базу данных в качестве параметра, чтобы вы могли ее имитировать.Я имею в виду это, если вы хотите провести модульное тестирование (не обращаясь к базе данных, что в некоторых случаях следует сделать позже).
    • IDataReader AgentInformation = GetAgentFromDatabase(agentId) — возможно, вы могли бы разделить извлечение Reader на какой-то другой класс, чтобы вы могли имитировать этот класс при тестировании фабричного кода.

    ИМО, как правило, вам следует беспокоиться только о том, чтобы сделать ваши общедоступные свойства / методы тестируемыми. То есть Пока работает Select (int agentId) , вам, как правило, все равно, как это происходит через GetAgentFromDatabase (int agentId) .

    То, что у вас есть, кажется разумным, так как я предполагаю, что его можно протестировать с помощью чего-то вроде следующего (при условии, что ваш класс называется AgentRepository)

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

    Что касается предложенных улучшений. Я бы порекомендовал разрешить изменение строки подключения AgentRepository с помощью открытого или внутреннего доступа.

    Предполагая, что вы пытаетесь протестировать общедоступный метод Select класса [NoName]..

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

    По моему мнению, метод GetAgentFromDatabase () не должен тестироваться дополнительным тестом, поскольку его код полностью покрывается тестом метода Select (). Нет веток, по которым мог бы пройти код, поэтому нет смысла создавать здесь дополнительный тест. Если метод GetAgentFromDatabase () вызывается из нескольких методов, вы должны проверить его самостоятельно.

    Лицензировано под: CC-BY-SA с атрибуция
    Не связан с StackOverflow
    scroll top