Тестовый класс с вызовом new() в Mockito
У меня есть устаревший класс, который содержит вызов new() для создания экземпляра LoginContext():
public class TestedClass {
public LoginContext login(String user, String password) {
LoginContext lc = new LoginContext("login", callbackHandler);
}
}
Я хочу протестировать этот класс, используя Mockito для макета LoginContext, так как он требует, чтобы перед созданием экземпляра были настроены средства безопасности JAAS, но я не уверен, как это сделать, не изменяя метод login() для экстернализации LoginContext. Возможно ли использовать Mockito для насмешки над классом LoginContext?
6 ответов
На будущее я бы порекомендовал Эран Харел ответ (рефакторинг, переезд new
на заводе что можно поиздеваться). Но если вы не хотите изменять исходный код, используйте очень удобную и уникальную функцию: шпионы. Из документации:
Вы можете создавать шпионов из реальных объектов. Когда вы используете шпиона, тогда вызываются реальные методы (если метод не был заглушен).
Настоящих шпионов следует использовать осторожно и время от времени, например, когда имеешь дело с устаревшим кодом.
В вашем случае вы должны написать:
TestedClass tc = spy(new TestedClass());
LoginContext lcMock = mock(LoginContext.class);
when(tc.login(anyString(), anyString())).thenReturn(lcMock);
Я полностью за решение Эрана Харела, и в случаях, когда это невозможно, предложение Томаша Нуркевича о шпионаже превосходно. Однако стоит отметить, что бывают ситуации, когда ни один из них не применим. Например, если login
метод был немного "более громким":
public class TestedClass {
public LoginContext login(String user, String password) {
LoginContext lc = new LoginContext("login", callbackHandler);
lc.doThis();
lc.doThat();
}
}
... и это был старый код, который не мог быть реорганизован для извлечения инициализации нового LoginContext
к его собственному методу и применить одно из вышеупомянутых решений.
Для полноты картины стоит упомянуть третий метод - использование PowerMock для инъекции фиктивного объекта, когда new
оператор называется. PowerMock не серебряная пуля, хотя. Он работает, применяя манипулирование байт-кодом к классам, которые он симулирует, что может быть изворотливой практикой, если в тестируемых классах используется манипуляция или отражение байт-кода, и, по крайней мере, из моего личного опыта, это приводит к снижению производительности теста. Опять же, если нет других вариантов, единственным вариантом должен быть хороший вариант:
@RunWith(PowerMockRunner.class)
@PrepareForTest(TestedClass.class)
public class TestedClassTest {
@Test
public void testLogin() {
LoginContext lcMock = mock(LoginContext.class);
whenNew(LoginContext.class).withArguments(anyString(), anyString()).thenReturn(lcMock);
TestedClass tc = new TestedClass();
tc.login ("something", "something else");
// test the login's logic
}
}
Вы можете использовать фабрику для создания контекста входа. Тогда вы можете издеваться над фабрикой и вернуть все, что вы хотите для вашего теста.
public class TestedClass {
private final LoginContextFactory loginContextFactory;
public TestedClass(final LoginContextFactory loginContextFactory) {
this.loginContextFactory = loginContextFactory;
}
public LoginContext login(String user, String password) {
LoginContext lc = loginContextFactory.createLoginContext();
}
}
public interface LoginContextFactory {
public LoginContext createLoginContext();
}
public class TestedClass {
public LoginContext login(String user, String password) {
LoginContext lc = new LoginContext("login", callbackHandler);
lc.doThis();
lc.doThat();
}
}
- Тестовый класс:
@RunWith(PowerMockRunner.class)
@PrepareForTest(TestedClass.class)
public class TestedClassTest {
@Test
public void testLogin() {
LoginContext lcMock = mock(LoginContext.class);
whenNew(LoginContext.class).withArguments(anyString(), anyString()).thenReturn(lcMock);
//comment: this is giving mock object ( lcMock )
TestedClass tc = new TestedClass();
tc.login ("something", "something else"); /// testing this method.
// test the login's logic
}
}
При вызове фактического метода tc.login ("something", "something else");
from testLogin() {
- Этот LoginContext lc установлен в ноль и выбрасывает NPE во время вызова lc.doThis();
Не то, что я знаю, но как насчет того, чтобы сделать что-то подобное, когда вы создаете экземпляр TestedClass, который вы хотите протестировать:
TestedClass toTest = new TestedClass() {
public LoginContext login(String user, String password) {
//return mocked LoginContext
}
};
Другой вариант - использовать Mockito для создания экземпляра TestedClass и позволить смоделированному экземпляру вернуть LoginContext.
Я оказался в особой ситуации, когда мой сценарий использования напоминал сценарий Мюрейника, но в итоге я использовал решение Томаша Нуркевича.
Вот как:
class TestedClass extends AARRGGHH {
public LoginContext login(String user, String password) {
LoginContext lc = new LoginContext("login", callbackHandler);
lc.doThis();
lc.doThat();
return lc;
}
}
Сейчас же, PowerMockRunner
ошибка инициализации TestedClass
потому что он простирается AARRGGHH
, который, в свою очередь, выполняет более контекстную инициализацию... Вы видите, куда меня вел этот путь: мне пришлось бы издеваться над несколькими слоями. Явно ОГРОМНЫЙ запах.
Я нашел хороший хак с минимальным рефакторингомTestedClass
: Я создал небольшой метод
LoginContext initLoginContext(String login, CallbackHandler callbackHandler) {
new lc = new LoginContext(login, callbackHandler);
}
Объем этого метода обязательно package
.
Тогда ваша тестовая заглушка будет выглядеть так:
LoginContext lcMock = mock(LoginContext.class)
TestedClass testClass = spy(new TestedClass(withAllNeededArgs))
doReturn(lcMock)
.when(testClass)
.initLoginContext("login", callbackHandler)
и дело в шляпе...
В ситуациях, когда тестируемый класс может быть изменен, и когда желательно избежать манипуляций с байтовым кодом, чтобы все было быстро или чтобы минимизировать сторонние зависимости, вот мое мнение об использовании фабрики для извлечения new
операция.
public class TestedClass {
interface PojoFactory { Pojo getNewPojo(); }
private final PojoFactory factory;
/** For use in production - nothing needs to change. */
public TestedClass() {
this.factory = new PojoFactory() {
@Override
public Pojo getNewPojo() {
return new Pojo();
}
};
}
/** For use in testing - provide a pojo factory. */
public TestedClass(PojoFactory factory) {
this.factory = factory;
}
public void doSomething() {
Pojo pojo = this.factory.getNewPojo();
anythingCouldHappen(pojo);
}
}
Благодаря этому вы сможете легко проверять, подтверждать и проверять вызовы объекта Pojo:
public void testSomething() {
Pojo testPojo = new Pojo();
TestedClass target = new TestedClass(new TestedClass.PojoFactory() {
@Override
public Pojo getNewPojo() {
return testPojo;
}
});
target.doSomething();
assertThat(testPojo.isLifeStillBeautiful(), is(true));
}
Единственный недостаток этого подхода может возникнуть, если TestClass
имеет несколько конструкторов, которые вы должны дублировать с дополнительным параметром.
По ТВЕРДЫМ причинам вы, вероятно, захотите вместо этого поместить интерфейс PojoFactory в класс Pojo, а также на производственную фабрику.
public class Pojo {
interface PojoFactory { Pojo getNewPojo(); }
public static final PojoFactory productionFactory =
new PojoFactory() {
@Override
public Pojo getNewPojo() {
return new Pojo();
}
};