Хобрук: Ваш путь к мастерству в программировании

Юнит-тест, интеграционный тест или проблема в дизайне?

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

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

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

  • In-memory DB — чтобы были данные для работы.
  • Инициализируйте регистратор компании - так как от этого зависит метод.
  • Инициализируйте репозиторий, спроектированный как singleton — это ворота функции в 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

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

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

  1. Тесты становятся удручающе медленными

  2. Окостенение конструкции наступает слишком рано

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

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

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

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

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

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

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

03.10.2010
  • большое спасибо за ваш комментарий. Вы имеете в виду, что я должен написать изолированный тест, который проверяет только то изменение, которое я внес в нашу реализацию DAL, то есть сеанс на запрос и транзакции, а затем предположить, что каждое место, которое использует проверенный DAL, будет работать правильно как моя реализация сама тестировалась? 03.10.2010
  • О, и еще одно, если у меня есть метод, который только изменяет состояние внешних объектов, конечно, через свою логику. Как и если бы вы провели модульное тестирование? Я могу обновить вопрос, чтобы показать образец такого метода, если он будет более понятен. 03.10.2010
  • Короче да. Если код вашей инфраструктуры работает для варианта использования A и правильно разделен, он также будет работать для вариантов использования B, C, D и, возможно, вам понадобится еще один тест для варианта использования E. Вы также можете посмотреть на Unitils: unitils.org/tutorial.html Он предлагает отличную поддержку для работы с тестовыми наборами данных и упрощает интеграционное тестирование. Только не переусердствуйте. 03.10.2010
  • Для обновлений: вы издеваетесь над объектами Session, Query, устанавливаете ожидаемые вызовы и возвращаете необходимые объекты, а также проверяете, что методы обновления вызываются с правильно обновленными объектами. 03.10.2010
  • Большое спасибо, я думаю, мне нужно еще немного изучить всю технику Mocking, но я думаю, что понял общую идею. 03.10.2010
  • Новые материалы

    Прогресс в технологии Трансформеров часть 3
    Многомасштабный управляющий сигнальный преобразователь для бесфазного синтеза движения (arXiv) Автор: Линтао Ван , Кун Ху , Лей Бай , Юй Дин , Ваньли Оуян , Чжиюн Ван . Аннотация:..

    Представляем поддержку компонентов Vue.js. Мгновенный HMR и многое другое.
    Хотя у FuseBox уже был плагин Vue, он был базовым и не имел многих функций, которые делали работу с Vue.js такой приятной. Однако с этим выпуском мы рады сообщить, что в FuseBox..

    Приключения в Javascript, часть 1
    Я продолжаю думать о том, чтобы писать больше, но чем больше я думаю об этом, тем меньше я это делаю. Итак, сегодня я перестал думать и начал писать. Отсюда можно только спускаться… В..

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

    Учебные заметки JavaScript Object Oriented Labs
    Вот моя седьмая неделя обучения программированию. После ruby ​​и его фреймворка rails я начал изучать самый популярный язык интерфейса — javascript. В отличие от ruby, javascript — это более..

    Разбор строк запроса в vue.js
    Иногда вам нужно получить данные из строк запроса, в этой статье показано, как это сделать. В жизни каждого дизайнера/разработчика наступает момент, когда им необходимо беспрепятственно..

    Предсказание моей следующей любимой книги 📚 Благодаря данным Goodreads и машинному обучению 👨‍💻
    «Если вы не любите читать, значит, вы не нашли нужную книгу». - J.K. Роулинг Эта статья сильно отличается от тех, к которым вы, возможно, привыкли . Мне очень понравилось поработать над..