Наконец-то руки дошли до разбирательств с юнит-тестированием. Не то, чтобы я раньше не писал юнит-тестов, писал, конечно, но это были простенькие тесты в уже настроенных проектах. А когда столкнулся с настройкой нового проекта — понял, что есть что поизучать. Одной статьёй не отделаться, придётся делать цикл 🙂
Начну с простой задачи, постепенно буду усложнять.Тестирование будем организовывать с помошью JUnit, EasyMock и PowerMock. Эта статья исходная, с исходной задачей и самым простым вариантом тестов. Вторая статья цикла описывает этот же тест, но с testng, а не с JUnit.
Итак, начальные условия, высосанные из пальца, но, тем не менее, демонстрирующие технологию: есть API AuthInterface, в котором реализован метод doAuth(String user, String password). Реализация простая, авторизация реализована неким внутренним классом InnerInterfaceImpl. Мы создаём экземпляр этого класса и вызываем метод authUser:
public boolean doAuth(final String userName, final String password) {
final InnerInterfaceImpl innerInterface = new InnerInterfaceImpl();
return innerInterface.authUser(userName, password);
}
Реализация этого внутреннего класса тоже не отличается сложностью, метод вохвращает true если переданные логин и пароль не null и соответстуют константным:
private static final String USER_NAME = "user";
private static final String USER_PASSWORD = "password";
/**
* do auth user
* @param userName user's name
* @param password user's password
* @return true if auth is successful
*/
public boolean authUser(final String userName, final String password) {
return userName != null && password != null && userName.equals(USER_NAME) && password.equals(USER_PASSWORD);
}
Это всё условности. Самое интересное дальше. Пропишем зависимости в pom.xml:
<properties>
<powermock.version>1.7.3</powermock.version>
<easymock.version>3.5.1</easymock.version>
</properties>
<dependencies>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>${easymock.version}</version>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>${powermock.version}</version>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-easymock</artifactId>
<version>${powermock.version}</version>
</dependency>
</dependencies>
Теперь можно написать три простых «прямых» теста:
- Передаём правильные логин/пароль, получаем авторизацию,
- Передаём неверные логин/пароль, получаем отказ в авторизации,
- Передаём вместо логина и пароля null, получаем отказ в авторизации, а не NullPointerException.
Выглядят эти тесты вот так:
/**
* Strict test, username and password are correct
*/
@Test
public void doAuthCorrectTest() {
final AuthInterface authInterface = new AuthInterfaceImpl();
final boolean isAuth = authInterface.doAuth(USER, PASSWORD);
//username and password are correct, we expect true
Assert.assertEquals(true, isAuth);
}
/**
* Strict test, username and password are incorrect
*/
@Test
public void doAuthIncorrectTest() {
final AuthInterface authInterface = new AuthInterfaceImpl();
final boolean isAuth = authInterface.doAuth(WRONG_USER, WRONG_PASSWORD);
//username and password are incorrect, we expect false
Assert.assertEquals(false, isAuth );
}
/**
* Strict test, username and password are incorrect
*/
@Test
public void doAuthNullsTest() {
final AuthInterface authInterface = new AuthInterfaceImpl();
final boolean isAuth = authInterface.doAuth(null, null);
//username and password are nulls, we expect false
Assert.assertEquals(false, isAuth );
}
}
Мы создаём экземпляр нашего API и вызываем метод doAuth с корректными, некорректными и null параметрами, полученный резацльтат проверяем с помошью Assert.assertEquals, первый параметр это ожидаемое нами значение, вторым параметром идёт фактическое значение. Если они совпадают, то тес пройден успешно, если нет — тест провален.
Это интересно и это работает, но в реальной жизни нам внутренний класс обычно недоступен. Это здесь мы прописали логин и пароль константами, реальная же авторизация идёт в БД, доступа к промышленной базе у разработчиков нет, тестовая база может быть недоступна и т.д. и т.п. Да и вообще, внутренний класс не наш, мы пишем только AuthInterface, мы хотим тестировать его и только его. Значит мы должны вместо InnerInterfaceImpl использовать заглушку, которая гарантированно вернёт нам именно то, что нам нужно. Эти заглушки и есть mock-и.
Итак, нам нужно, чтобы тест вместо экземпляра InnerInterfaceImpl создал заглушку, которая на запрос InnerInterfaceImpl.authUser вернёт то, что нужно нам. Первую часть задуманного (создать заглушку вместо реального экземпляра) нам поможет исполнить PowerMock, вторую часть (вернуть то, что нужно нам), сделает EasyMock.
Для начала напишем тест, в котором убедимся, что работает именно заглушка, а не реальная реализация. Для этого инвертируем ожидания — передадим некорректные значения, но пропишем, что их результат — true. В реальности, конечно, такие тесты не нужны. Это тест нашего теста :-). Выглядит это вот так:
/**
* Mocking test, passing wrong values, but expecting true
*
* @throws Exception
*/
@Test
public void doAuthMockAbsolutlyWrongTest() throws Exception {
final InnerInterfaceImpl innerInterfaceMock = createMock(InnerInterfaceImpl.class);
PowerMock.expectNew(InnerInterfaceImpl.class).andReturn(innerInterfaceMock).anyTimes();
expect(innerInterfaceMock.authUser(WRONG_USER, WRONG_PASSWORD)).andReturn(true).anyTimes();
final AuthInterface authInterface = new AuthInterfaceImpl();
PowerMock.replay(innerInterfaceMock, InnerInterfaceImpl.class);
final boolean isAuth = authInterface.doAuth(WRONG_USER, WRONG_PASSWORD);
PowerMock.verify(innerInterfaceMock);
Assert.assertEquals(true, isAuth);
}
Разберёмся с тем, что происходит:
- Говорим, что классы нужно запускать через PowerMock: @RunWith(PowerMockRunner.class)
- Создаём заглушку внуреннего класса PowerMock.createMock(InnerInterfaceImpl.class)
- Говорим, что при вызове конструктора внутреннего класса, нужно вернуть заглушку: PowerMock.expectNew(InnerInterfaceImpl.class).andReturn(innerInterfaceMock).anyTimes()
- Говорим, что при вызове authUser этой заглушки с некорректными параметрами, нужно вернуть true: expect(innerInterfaceMock.authUser(WRONG_USER, WRONG_PASSWORD)).andReturn(true).anyTimes()
- Создаём экземпляр нашего класса new AuthInterfaceImpl() и инициализируем наш mock PowerMock.replay(innerInterfaceMock, InnerInterfaceImpl.class). Теперь при инициализации внутреннего класа подставится наша заглушка.
- Дальше всё просто. Получаем результат по некорректным для реальной реализации параметрам: isAuth = authInterface.doAuth(WRONG_USER, WRONG_PASSWORD)
- Убеждаемся, что наш mock был вызван: PowerMock.verify(innerInterfaceMock)
- И убеждаемся в том, что, несмотря на некоректные для реальной реализации параметры, результат true: Assert.assertEquals(true, isAuth)
Всё работает ровно так, как и ожидалось!
Теперь напишу «нормальный» тест, который проверяет, что результат соответствует параметрам:
/**
* Mocking test:
* <ul>
* <li>passing correct values, expecting true</li>
* <li>passing wrong values, expecting false</li>
* <li>passing nulls, expecting false</li>
* </ul>
*
* @throws Exception
*/
@Test
public void doAuthMockTest() throws Exception {
final InnerInterfaceImpl innerInterfaceMock = PowerMock.createMock(InnerInterfaceImpl.class);
PowerMock.expectNew(InnerInterfaceImpl.class).andReturn(innerInterfaceMock).anyTimes();
expect(innerInterfaceMock.authUser(USER, PASSWORD)).andReturn(true).anyTimes();
expect(innerInterfaceMock.authUser(WRONG_USER, WRONG_PASSWORD)).andReturn(false).anyTimes();
expect(innerInterfaceMock.authUser(null, null)).andReturn(false).anyTimes();
final AuthInterface authInterface = new AuthInterfaceImpl();
PowerMock.replay(innerInterfaceMock, InnerInterfaceImpl.class);
final boolean isAuthCorrect = authInterface.doAuth(USER, PASSWORD);
final boolean isAuthIncorrect = authInterface.doAuth(WRONG_USER, WRONG_PASSWORD);
final boolean isAuthNulls = authInterface.doAuth(null, null);
PowerMock.verify(innerInterfaceMock);
Assert.assertEquals(true, isAuthCorrect);
Assert.assertEquals(false, isAuthIncorrect);
Assert.assertEquals(false, isAuthNulls);
}
Запускаем mvn test и убеждаемся, что все тесты проходят корректно:
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
Подвожу итог: если мы в тестовом окружении не можем или не хотим обращаться к реальной имплиментации класса, мы можем его «замокировать», т.е. заменить заглушкой. Это оправданно тогда и только тогда, когда мокируется действительно внешний класс, который имеет опосредованное значение к разабатываемому нами алгоритму. Другими словами, мы тестируем свой алгоритм и полагаемся на то, что внешние моули сами написали для себя свои тесты.
Исходники — на моём гитхабе, ветка JUnit. Там же есть ветка testng, описание отличий от JUnit — во второй части.