Модульный тест, интеграционный тест или проблема в дизайне? - PullRequest
2 голосов
/ 03 октября 2010

Я написал свой первый юнит-тест и думаю, что он слишком зависит от других модулей, и я не уверен, так ли это, потому что:

  • Это сложный тест
  • Я на самом деле написал интеграционный тест или
  • У меня проблема с дизайном

Сначала я скажу, что, хотя у меня есть около 4 лет опыта разработки, я никогда не учился и не обучался автоматическому тестированию.
Я только что закончил серьезное изменение в нашей реализации DAL с Hibernate, и мой коллега предложил мне написать юнит-тесты для новых деталей.
Основное изменение касалось переключения на шаблон «Сессия-на-запрос» и более конструктивного использования транзакций приложения.
Из-за характера вышеуказанного изменения модульное тестирование начинается в точке, когда поступает конкретный запрос и начинает транзакцию, а тест заканчивается после завершения транзакции и проверяет, выполнила ли транзакция изменения, которые она должна была сделать.
Этот тест включает в себя инициализацию следующих объектов:

  • БД в памяти - так что будут данные для работы.
  • Инициализировать регистратор компании - так как метод зависит от него.
  • Инициализируйте репозиторий, сконструированный как одноэлементный - это ворота функции в DAL, но он также хранит другие вещи, так что это большой объект для создания.
  • Инициализировать обработчик запросов, который также является одноэлементным - здесь содержится метод, который нужно протестировать.

Я думаю, что на самом деле я написал интеграционный тест, так как мне нужно инициализировать БД, Hibernate и репозиторий, но я не уверен, как мог бы написать его иначе, учитывая обстоятельства, когда в тестируемом методе используются все эти объекты для его действия, и мне интересно посмотреть, как выполняется обработка транзакций (что делается на проверенном методе).

Буду признателен за все комментарии и мысли и с удовольствием разработаю или проясню все, если они недостаточно ясны.

Спасибо
Итай

P.S. HibernateSessionFactory фактически является широко известным HibernateUtil из книги Hibernate In Action, ошибочно названным по историческим причинам.

public class AdminMessageRepositoryUpdaterTest {
private static WardId wardId;
private static EmployeeId employeeId;
private static WardId prevWardId;
private static EmployeeId prevEmployeeId;

@Test
public void testHandleEmployeeLoginToWard(){
    AgentEmployeesWardsEngine agentEmployeesWardsEngine = new AgentEmployeesWardsEngine();
    AgentEngine agentEngine = new AgentEngine();
    //Remove all entries from AgentEmployeesWards table
    HibernateSessionFactory.beginTransaction();
    for (Agent agent : agentEngine.findAll()){
        agentEmployeesWardsEngine.removeAgentEntries(agent.getId());
    }
    HibernateSessionFactory.commitTransaction();//no need to try catch as this is done in a controlled environment
    int i=0;
    //build expectedSet
    Set<AgentEmployeesWards> expectedMappingsToChangeSet = new HashSet<AgentEmployeesWards>();
    //Mappings which should have ward updated
    expectedMappingsToChangeSet.add(new AgentEmployeesWards(new AgentId(1).getValue(), employeeId.getValue(), prevWardId.getValue(), true, TimestampUtils.getTimestamp(), i++));
    expectedMappingsToChangeSet.add(new AgentEmployeesWards(new AgentId(2).getValue(), employeeId.getValue(), prevWardId.getValue(), true, TimestampUtils.getTimestamp(), i++));
    //Mappings which should have employee updated
    expectedMappingsToChangeSet.add(new AgentEmployeesWards(new AgentId(3).getValue(), prevEmployeeId .getValue(), wardId.getValue(), false, TimestampUtils.getTimestamp(), i++));
    expectedMappingsToChangeSet.add(new AgentEmployeesWards(new AgentId(4).getValue(), prevEmployeeId.getValue(), wardId.getValue(), false, TimestampUtils.getTimestamp(), i++));

    //Prepare clean data for persistence
    Set<AgentEmployeesWards> cleanSet = new HashSet<AgentEmployeesWards>(expectedMappingsToChangeSet);
    //Mappings which should NOT have ward updated
    cleanSet.add(new AgentEmployeesWards(new AgentId(5).getValue(), employeeId.getValue(), prevWardId.getValue(), false, TimestampUtils.getTimestamp(), i++));
    cleanSet.add(new AgentEmployeesWards(new AgentId(6).getValue(), employeeId.getValue(), prevWardId.getValue(), false, TimestampUtils.getTimestamp(), i++));
    //Mappings which should NOT have employee updated
    cleanSet.add(new AgentEmployeesWards(new AgentId(7).getValue(), prevEmployeeId .getValue(), wardId.getValue(), true, TimestampUtils.getTimestamp(), i++));
    cleanSet.add(new AgentEmployeesWards(new AgentId(8).getValue(), prevEmployeeId.getValue(), wardId.getValue(), true, TimestampUtils.getTimestamp(), i++));
    HibernateSessionFactory.beginTransaction();
    for (AgentEmployeesWards agentEmployeesWards : cleanSet){
        agentEmployeesWardsEngine.saveNewAgentEmployeesWardsEntry(agentEmployeesWards);
    }
    HibernateSessionFactory.commitTransaction();//no need to try catch as this is done in a controlled environment
    //Close the session as to neutralize first-level-cache issues
    HibernateSessionFactory.closeSession();
    //Perform the action so it can be tested
    AdminMessageReposityUpdater.getInstance().handleEmployeeLoginToWard(employeeId, wardId, TimestampUtils.getTimestamp());

    //Close the session as to neutralize first-level-cache issues
    HibernateSessionFactory.closeSession();

    //Load actualSet from DAL
    Set<AgentEmployeesWards> actualSet = new HashSet<AgentEmployeesWards>(agentEmployeesWardsEngine.findByPrimaryEmployeeId(employeeId));
    actualSet.addAll(agentEmployeesWardsEngine.findByPrimaryWardId(wardId));

    //Prepare expected
    Set<AgentEmployeesWards> expectedSet = new HashSet<AgentEmployeesWards>();
    for (AgentEmployeesWards agentEmployeesWards : expectedMappingsToChangeSet){
        //We need to copy as the wardId and employeeId are properties which comprise the equals method of the class and so 
        //they cannot be changed while in a Set
        AgentEmployeesWards agentEmployeesWardsCopy = new AgentEmployeesWards(agentEmployeesWards);
        if (agentEmployeesWardsCopy.isEmployeePrimary()){
            //If this is a employee primary we want it to be updated to the new org-unit id
            agentEmployeesWardsCopy.setWardId(wardId.getValue());
        } else {
            //Otherwise we want it to be updated to the new employee id
            agentEmployeesWardsCopy.setEmployeeId(employeeId.getValue());
        }
        expectedSet.add(agentEmployeesWardsCopy);
    }
     //Assert between actualSet and expectedSet
    // Assert actual database table match expected table
   assertEquals(expectedSet, actualSet);


}
@BeforeClass
public static void setUpBeforeClass() throws SQLException,ClassNotFoundException{
    Class.forName("org.h2.Driver");
    Connection conn = DriverManager.getConnection("jdbc:h2:mem:MyCompany", "sa", "");

    ConfigurationDAO configDAO = new ConfigurationDAO();
    HibernateSessionFactory.beginTransaction();
    configDAO.attachDirty(new Configuration("All","Log", "Level", "Info",null));
    configDAO.attachDirty(new Configuration("All","Log", "console", "True",null));
    configDAO.attachDirty(new Configuration("All","Log", "File", "False",null));

    HibernateSessionFactory.commitTransaction();
    Logger log = new Logger();
    Server.getInstance().initialize(log);
    Repository.getInstance().initialize(log);
    AdminMessageReposityUpdater.getInstance().initialize(log);

    AdminEngine adminEngine = new AdminEngine();
    EmployeeEngine employeeEngine = new EmployeeEngine();
    HibernateSessionFactory.beginTransaction();
    Ward testWard = new Ward("testWard", 1, "Sales", -1, null);
    adminEngine.addWard(testWard);
    wardId = new WardId(testWard.getId());
    Ward prevWard = new Ward("prevWard", 1, "Finance", -1, null);
    adminEngine.addWard(prevWard);
    prevWardId = new WardId(prevWard.getId());

    Employee testEmployee = new Employee("testEmployee", "test", null, "employee", "f", prevWardId.getValue(), null, false, true);
    employeeEngine.setEmployee(testEmployee);
    employeeId = new EmployeeId(testEmployee.getId());

    Employee prevEmployee = new Employee("prevEmployee", "prev", null, "employee", "f", wardId.getValue(), null, false, true);
    employeeEngine.setEmployee(prevEmployee);
    prevEmployeeId = new EmployeeId(prevEmployee.getId());
    HibernateSessionFactory.commitTransaction();
    HibernateSessionFactory.closeSession();
}
@AfterClass
public static void tearDownAfterClass(){
    AdminEngine adminEngine = new AdminEngine();
    EmployeeEngine employeeEngine = new EmployeeEngine();
    HibernateSessionFactory.beginTransaction();
    employeeEngine.removeEmployeeById(employeeId);
    employeeEngine.removeEmployeeById(prevEmployeeId);
    adminEngine.removeWardById(wardId);
    adminEngine.removeWardById(prevWardId);
    HibernateSessionFactory.commitTransaction();
    HibernateSessionFactory.closeSession();
}
}

1 Ответ

4 голосов
/ 03 октября 2010

Да, это определенно интеграционный тест.В интеграционных тестах нет ничего плохого, и они являются важной частью стратегии тестирования, но они должны быть ограничены проверкой правильности сборки модулей и правильной настройки конфигурации.

Если вы начнете использовать их длятестируйте функциональность, вы получите слишком много из них, и произойдут 2 очень плохих вещи:

  1. Тесты становятся чрезвычайно медленными

  2. Дизайн окостенения устанавливается вслишком рано

Последняя проблема заключается в том, что теперь вы объединяете свой проект в интеграционных тестах, даже если сами модули идеально отделены друг от друга.Если вы найдете возможность провести рефакторинг, скорее всего, он сломает дюжину интеграционных тестов, и вы либо не найдете смелости, либо руководство помешает вам вычистить (синдром «Я работаю! Не трогай»)).

Решение состоит в том, чтобы провести модульное тестирование всех написанных вами частей путем "макетирования" среды.Есть хорошие фреймворки, которые помогают создавать макеты объектов на лету, я лично часто использую EasyMock.Затем вы описываете взаимодействия с остальным миром, проверяя функциональность ваших методов

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

Нет смысла проводить модульное тестирование кода инфраструктуры, поскольку он, вероятно, уже прошел модульное тестирование, и с этим ничего не поделаешь.

Затем добавьте 1 или 2 целевых интеграционных теста, чтобы убедиться, что все части работают должным образом, запросы возвращают нужные объекты, транзакции обрабатываются правильно и т. Д. *

Это уравновешивает необходимость проверки всегоРаботает в собранном виде, с возможностью рефакторинга, сокращая время тестирования и создавая слабосвязанные модули.

Однако для его выполнения требуется некоторый опыт.Я бы порекомендовал найти опытного разработчика, который делал это раньше и предлагал напитки в обмен на наставничество в этой области.Задайте много вопросов.

...