Статьи

Модульное тестирование с TestNG и JMockit, часть 2

Это руководство является второй частью серии из двух частей о TestNG и jmockit. Предыдущий учебник (найденный здесь ) охватывал классический сценарий JUnit и EasyMock, только с TestNG и jmockit. Хотя вы можете проводить такое тестирование с помощью этих двух технологий, это не является их сильной стороной. В этом руководстве мы рассмотрим некоторые более продвинутые функции TestNG и будем использовать способность jmockit «переназначать» класс в вашей JVM для обработки более надежного набора сценариев тестирования.

Настроить

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

Сценарий

Этот сценарий такой же, как в предыдущем уроке. Однако мы будем реализовывать решение немного иначе. Вместо использования Spring-инъекций зависимостей нашего UserDAO наш сервис будет создавать его самостоятельно. Просто для справки, диаграмма классов для сценария ниже.

Диаграмма классов TestNG

Интерфейсы

Как было сказано ранее, мы будем тестировать тот же сценарий, что и в предыдущем уроке. Для обзора мы начнем наше кодирование с определения двух и реализации интерфейсов, LoginService и UserDAO. Для LoginService у нас есть единственный метод, который принимает String userName и String пароль и возвращает логическое значение (true, если пользователь был найден, false, если это не так). Интерфейс выглядит так:

/**
* Provides authenticated related processing.
*/
public interface LoginService {

/**
* Handles a request to login. Passwords are stored as an MD5 Hash in
* this system. The login service creates a hash based on the paramters
* received and looks up the user. If a user with the same userName and
* password hash are found, true is returned, else false is returned.
*
* @parameter userName
* @parameter password
* @return boolean
*/
boolean login(String userName, String password);
}

 

Интерфейс UserDAO будет очень похож на LoginService. У него будет единственный метод, который принимает userName и хэш. Хеш — это хешированная версия пароля MD5, предоставляемая вышеуказанным сервисом.

/**
* Provides database access for login related functions
*/
public interface UserDAO {

/**
* Loads a User object for the record that
* is returned with the same userName and password.
*
* @parameter userName
* @parameter password
* @return User
*/
User loadByUsernameAndPassword(String userName, String password);
}

Издеваться без инъекций

jmockit основан на концепции переопределения класса JVM. В JDK 1.5 был создан класс java.lang.instrument.Instrumentation. Это позволяет вам «переназначить» определение класса в JVM программно. Примером будет, если я определю ClassA и ClassB. Я могу сказать JVM «Если запрашивается экземпляр ClassA, дайте им ClassB». Это описание очень высокого уровня. Пожалуйста, обратитесь к javadocs здесь для более подробной информации.

Ниже приведена реализация LoginServiceImpl, с которой мы начнем. В настоящее время он реализован так, чтобы принимать UserDAO для внедрения через некоторую форму внедрения зависимости (Spring и т. Д.).

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;


public class LoginServiceImpl implements LoginService {
UserDAO userDao;

public void setUserDao(UserDAO userDao) {
this.userDao = userDao;
}

public boolean login(String userName, String password) {
boolean valid = false;
try {
String passwordHash = null;
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(password.getBytes());
passwordHash = new String(md5.digest());

User results =
userDao.loadByUsernameAndPassword(userName, passwordHash);
if(results != null) {
valid = true;
}
} catch (NoSuchAlgorithmException ignore) {}

return valid;
}
}

Тест для вышеупомянутого метода ниже. Эти предметы вместе составляют нашу отправную точку.

import mockit.Expectations;

import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;

public class LoginServiceTest extends Expectations {

private LoginServiceImpl service;
UserDAO mockDao;

@BeforeTest
public void setupMocks() {
service = new LoginServiceImpl();
service.setUserDao( mockDao );
}

/**
* This method will test the "rosy" scenario of passing a valid
* username and password and retrieveing the user. Once the user
* is returned to the service, the service will return true to
* the caller.
*/
@Test
public void testRosyScenario() {
User results = new User();
String userName = "testUserName";
String password = "testPassword";
String passwordHash =
"�Ӷ&I7���Ni=.";

invokeReturning(
mockDao.loadByUsernameAndPassword( userName,
passwordHash ),
results );
endRecording();

assert service.login( userName, password ) :
"Expected true, but was false";
}
}

Чтобы начать с изменений нашего кода, мы сначала хотим реорганизовать наш тест, чтобы ожидать «переназначения» нашего UserDAOImpl в нашу модель. Для этого давайте начнем с того, что нам больше не нужно. Нам больше не нужно расширяться Expectations. Поскольку мы будем определять макет вручную, он не используется. Мы также можем удалить объявление UserDAO в качестве поля и все ссылки на него, включая создание макета и внедрение в setupMocksметод, а также invokeReturningвызов и endRecordingвызов testRosyScenario. То, что вы должны остаться с ниже:

import mockit.Mockit;

import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;

public class LoginServiceTest {

private LoginServiceImpl service;

@BeforeTest
public void setupMocks() {
service = new LoginServiceImpl();
}

/**
* This method will test the "rosy" scenario of
* passing a valid username and password and
* retrieveing the user. Once the user is returned
* to the service, the service will return true to
* the caller.
*/
@Test
public void testRosyScenario() {
final String userName = "testUserName";

assert service.login( userName, "testPassword" ) :
"Expected true, but was false";
}
}

Теперь давайте создадим наш макет, и это ожидание. Для этого мы будем использовать mockit.Mockit.redefineMethods()метод. Этот статический метод принимает два параметра: класс, который вы хотите переназначить, и класс, в который вы хотите переназначить его. В нашем случае мы хотим переназначить класс UserDaoImpl в макет, который мы определим встроенным (для простоты этого урока). Вы можете увидеть обновленный метод тестирования ниже:

	/**
* This method will test the "rosy" scenario of
* passing a valid username and password and
* retrieveing the user. Once the user is returned
* to the service, the service will return true to
* the caller.
*/
@Test
public void testRosyScenario() {
final String userName = "testUserName";

Mockit.redefineMethods( UserDaoImpl.class, new Object() {
} );

assert service.login( userName, "testPassword" ) :
"Expected true, but was false";
}

Теперь давайте определим метод, который будет вызывать наш сервис. В этом случае мы будем утверждать, что ожидаемые нами параметры были переданы, и возвращать пустой объект User (поскольку нам важно только, был ли возвращен объект User или нет).

/**
* This method will test the "rosy" scenario of
* passing a valid username and password and
* retrieveing the user. Once the user is returned
* to the service, the service will return true to
* the caller.
*/
@Test
public void testRosyScenario() {
final String userName = "testUserName";

Mockit.redefineMethods( UserDaoImpl.class, new Object() {
public User loadByUsernameAndPassword( String un, String password ) {
assert un.equals( userName ) : "Username did not match";
assert "þÓ¶&I7€€³Ni=.".equals( password ) :
"Password hash did not match";
return new User();
}
} );

assert service.login( userName, "testPassword" ) :
"Expected true, but was false";
}

Если вы выполните тест в этот момент, вы должны получить NullPointerException. Это связано с тем, что мы не реорганизовали сервис для создания экземпляра DAO (он все еще ищет его для внедрения). Чтобы реорганизовать сервис, мы сделаем небольшое изменение кода:

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;


public class LoginServiceImpl implements LoginService {

public boolean login(String userName, String password) {
boolean valid = false;
try {
String passwordHash = null;
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(password.getBytes());
passwordHash = new String(md5.digest());

UserDAO userDao = new UserDaoImpl();

User results =
userDao.loadByUsernameAndPassword(userName, passwordHash);
if(results != null) {
valid = true;
}
} catch (NoSuchAlgorithmException ignore) {}

return valid;
}
}

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

Тестовая группировка в TestNG

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

/**
* This method will test the negative of the "rosy"
* scenario of passing a valid username and password and
* retrieving the user. Once the user is returned
* to the service, the service will return true to
* the caller.
*/
@Test
public void testNotFoundScenario() {
final String userName = "notFoundUser";

Mockit.redefineMethods( UserDaoImpl.class, new Object() {
public User loadByUsernameAndPassword( String un, String password ) {
assert un.equals( userName ) : "Username did not match";
assert "þÓ¶&I7€€³Ni=.".equals( password ) :
"Password hash did not match";
return null;
}
} );

assert !service.login( userName, "testPassword" ) :
"Expected false, but was true";
}

Если вы запускаете свой тестовый класс с добавленным выше методом, он должен пройти. Итак, для нашего примера, мы собираемся сгруппировать положительные сценарии вместе и отрицательные вместе и создать группу, которая также управляет ими. Для этого мы добавим groupsпараметр к каждой @Testаннотации, а также @BeforeTestаннотацию, которая есть у нас в setupMocksметоде. У нас будет одна группа с именем положительная и одна группа с именем отрицательная. У нас будет третья группа с именем all, которая будет выполнять все тесты. Ниже обновленный код:

@BeforeTest(groups ={"positive", "all", "negative"})
public void setupMocks() {
service = new LoginServiceImpl();
}

/**
* This method will test the "rosy" scenario of
* passing a valid username and password and
* retrieveing the user. Once the user is returned
* to the service, the service will return true to
* the caller.



*/
@Test(groups = {"positive", "all"})
public void testRosyScenario() {
final String userName = "testUserName";

Mockit.redefineMethods( UserDaoImpl.class, new Object() {
public User loadByUsernameAndPassword( String un, String password ) {
assert un.equals( userName ) : "Username did not match";
assert "þÓ¶&I7€€³Ni=.".equals( password ) :
"Password hash did not match";
return new User();
}
} );

assert service.login( userName, "testPassword" ) :
"Expected true, but was false";
}

/**
* This method will test the negative of the "rosy"
* scenario of passing a valid username and password and
* retrieving the user. Once the user is returned
* to the service, the service will return true to
* the caller.
*/
@Test(groups = {"negative", "all"})
public void testNotFoundScenario() {
final String userName = "notFoundUser";

Mockit.redefineMethods( UserDaoImpl.class, new Object() {
public User loadByUsernameAndPassword( String un, String password ) {
assert un.equals( userName ) : "Username did not match";
assert "þÓ¶&I7€€³Ni=.".equals( password ) :
"Password hash did not match";
return null;
}
} );

assert !service.login( userName, "testPassword" ) :
"Expected false, but was true";
}

Теперь мы можем запустить наш тест четырьмя различными способами, мы можем запустить его с группой all, группой отрицательной, группой положительной или по умолчанию (что является всеми тестами). Давайте начнем с запуска только положительных. Чтобы запустить только положительную группу, щелкните правой кнопкой мыши по вашему тестовому классу и выберите Run As -> Open Run Dialog …. На вкладке Test у вас есть возможность выбрать класс, группу или набор. В этом случае мы собираемся выбрать радиокнопку группы и нажать кнопку «Обзор» справа от этой строки. Вы заметите, что Eclipse предлагает вам три варианта: отрицательный, положительный и все, три группы, которые мы определили в нашем тесте. Выберите положительный вариант и нажмите ОК. Нажмите Run, чтобы выполнить тест. Вы заметите, что это удачно и только один тест был выполнен, наш положительный. Если вы сделаете вышеуказанные шаги снова,Вы можете выбрать либо отрицательный, либо все группы для выполнения.

Вы заметите, что мы должны были включить groupsпараметр в @BeforeTestаннотацию. Это очень полезно, поскольку вы можете указать методы настройки для каждой группы. Группы также не ограничены одним файлом класса. Группы могут охватывать несколько тестовых классов. Делая это, вы можете определить группы для всего проекта (например, проверки в тестах, непрерывные интеграционные тесты, интеграционные тесты и т. Д.), Которые можно запускать в зависимости от ситуации. Группы также имеют иерархию, которую можно расширять, когда вы реализуете группы на уровне класса (а не на уровне метода, как в этом учебном пособии).

Вывод

Функции TestNG и jmockit предоставляют очень надежный набор функций, который позволяет тестировать практически все возможные сценарии. Они позволяют гибко проектировать вашу систему наилучшим образом, вместо того, чтобы жертвовать ради возможности тестирования. То, что мы рассмотрели в этом и предыдущем уроках, — лишь вершина айсберга. Я надеюсь, что это даст вам стимул взглянуть на все TestNG и jmockit.