Как я могу реорганизовать этот метод фабричного типа и вызов базы данных для проверки? - PullRequest
12 голосов
/ 05 августа 2009

Я пытаюсь научиться выполнять модульное тестирование и издевательство. Я понимаю некоторые принципы 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? Будем весьма благодарны за любые предложения или указатели на внешние ссылки.

Ответы [ 6 ]

9 голосов
/ 05 августа 2009

Вы правы относительно перемещения 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 , но это тема отдельного вопроса. НТН

1 голос
/ 05 августа 2009

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

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

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

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

  3. Некоторое время назад я использовал 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")]. Убедитесь, что с определенной базой данных работает только один набор тестов.

0 голосов
/ 05 августа 2009

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

0 голосов
/ 05 августа 2009

Предполагается, что вы пытаетесь протестировать открытый метод 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...
0 голосов
/ 05 августа 2009

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

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

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

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

0 голосов
/ 05 августа 2009

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

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