2013-08-23 2 views
2

Мое приложение использует баночку третьей стороны (нет доступа к источнику, и т.д.) У меня есть завод, который создает объект (назовем его Foo) правильно из настроек, т.е.испытания на заводе класса 3 партии

public FooFactoryImpl implements FooFactory { 
    private final Settings settings; 
    private final OtherDependency other; 

    @Inject 
    public FooFactoryImpl(Settings settings, OtherDependency other) { 
     this.settings = settings; 
     this.other = other; 
    } 

    public Foo create(String theirArg) { 
     Foo newFoo = new Foo(theirArg); // there is no no-arg constructor 

     // This isn't exactly the way I do it but this is shorter and close enough 
     newFoo.setParamOne(settings.get("ParamOne")); 
     newFoo.setParamTwo(settings.get("ParamTwo")); 
     // etc. 
    } 
} 

Я бы хотел, чтобы блок протестировал эту фабрику с помощью Mockito - убедитесь, что созданный объект настроен правильно. Но, конечно, я столкнулся с this problem; то есть, потому что мой завод называет new, я не могу ввести шпион.

Одним из возможных решений является ввести что-то вроде:

public FooFactoryDumb implements FooFactory { 
    public Foo create(String theirArg) { 
     return new Foo(theirArg); 
    } 
} 

А потом что-то вроде:

public FooFactoryImpl implements FooFactory { 
    @Inject @Dumb private FooFactory inner; 

    // snip, see above 

    public create(String theirArg) { 
     Foo newFoo = inner.create(theirArg); 
     // etc. 
    } 
} 

Это, кажется, как много шаблонного кода просто чтобы дать возможность модульного тестирования. Это smells bad для меня, но я могу ошибаться. Есть ли способ лучше?

ответ

0

Ну, оказывается, что мне пришлось использовать PowerMock, потому что методы третьей стороны были окончательными. Так как я уже использую PowerMock, я понял, что я могу просто сделать это:

@Before 
public void setUp() throws Exception { 
    Foo toReturn = PowerMockito.mock(Foo.class); 
    PowerMockito.whenNew(Foo.class).withAnyArguments().thenReturn(toReturn); 
} 

И тогда я не должен трогать мой исходный класс вообще.

Примечание: Если вы сделаете это, вы должны подготовить оба класса для PowerMock, то есть сделать

@PrepareForTest({ Foo.class, FooFactoryImpl.class }) 
2

Существует аналогичный, но более простой способ сделать это: добавить защищенный метод к вашей фабрике, чтобы создать Foo:

protected Foo create(String theirArg){ 
    return new Foo(theirArg); 
} 

затем в тестах вашего завода, создать Test Double вашего FactoryImpl и переопределение создавание метод:

private class FooFactoryImplTestDouble extends FooFactoryImpl{ 
    ... 
    @Override 
    protected Foo create(String theirArg){ 

     //create and return your spy here 
    } 
} 
+0

Это определенно проще. Наверное, у меня слишком много инъекций в зависимости от мозга :) Собираюсь посмотреть, есть ли другие хорошие ответы, прежде чем я соглашусь. – durron597

+0

Я сделал это, и это сработало. И мне не нужно было менять свой модуль или создавать аннотации. – durron597

+0

Это вполне разумная идея, если рефакторинг фабрики является опцией. Однако вам не нужен класс Test Double; вы можете просто использовать шпиона Mockito в 'FooFactoryImpl' и переопределить метод create. Это должно сделать ваши намерения более четкими в вашем тестовом коде. –

0

Создайте новый класс:

public class FooFactory3rd { 
    public Foo create3rdParty(String theirArg) { 
     return new Foo(theirArg); 
    } 
} 

Затем измените свой класс:

public FooFactoryImpl implements FooFactory { 
    private final Settings settings; 
    private final OtherDependency other; 
    private final FooFactory3rd fooFactory3rd; 

    @Inject 
    public FooFactoryImpl(Settings settings, OtherDependency other, FooFactory3rd fooFactory3rd) { 
     this.settings = settings; 
     this.other = other; 
     this.fooFactory3rd = fooFactory3rd; 
    } 

    public Foo create(String theirArg) { 
     Foo newFoo = fooFactory3rd.create3rdParty(theirArg); 

     // This isn't exactly the way I do it but this is shorter and close enough 
     newFoo.setParamOne(settings.get("ParamOne")); 
     newFoo.setParamTwo(settings.get("ParamTwo")); 
     // etc. 
    } 
} 

И в тестовом коде:

Foo fooMock = mock(Foo.class); 
FooFactory3rd fooFactory3rdMock = mock(FooFactory3rd.class); 
when(fooFactory3rdMock.create3rdParty(any(String.class)).thenReturn(fooMock); 

FooFactoryImpl fooFactoryImpl = new FooFactoryImpl(settings, other, fooFactory3rdMock); 
fooFactoryImpl.create("any string"); 

Таким образом, вы можете придать свой fooMock. Когда вы вызываете fooFactoryImpl.create("any string"), ваше издеваемое Foo вызывается под обложкой.

Или, если вы хотите идти дальше, вам даже не нужен конструктор arg FooFactory3rd. Просто объявите

private final FooFactory3rd fooFactory3rd = new FooFactory3rd(); 

И в своем тесте используйте отражение, чтобы изменить его на издеваемую FooFactory3rd.

+0

Я не хочу вводить макет, я хочу ввести ** шпион **, я хочу убедиться, что фабрика вызвала правильные методы набора и так далее. Кроме того, чтобы убедиться, что создан «работоспособный, законченный и полный» объект Foo, зависит от гораздо большего количества вещей, включая вывод методов get, которые я не контролирую, и так далее. – durron597

+0

См. Отредактированный ответ. – KKKCoder

+0

Я вижу точку. Тем не менее, модульное тестирование, как и его название, предполагает тестирование единицы кода. Если Foo foo = new Foo (arg) является частью вашего метода create. Вы должны проверить это, а не вводить насмешку. Одна из потенциальных проблем заключается в том, что новый сам может потерпеть неудачу, используя макет просто означает, что вы просто проверяете часть метода «create». Еще раз, если новое является частью вашего устройства, ваш модульный тест должен протестировать его как есть. – KKKCoder

0

Сделайте шаг назад и подумать о том, что договор FooFactoryImpl есть. Это значит, что он должен создать полностью функциональный Foo, что бы это ни значило.Таким образом, если договор Foo заключается в том, что он выполняет X, Y и Z, то договор FooFactoryImpl заключается в том, что он создает объекты, которые делают X, Y и Z.

Это случай такого рода испытаний в который SUT состоит из более чем одного класса. Меня не волнует, называете ли вы это единичным тестом, интеграционным тестом, тестом подсистемы, тестом на совместную работу или другим именем. Дело в том, что единственным значимым тестом FooFactoryImpl является тест, который также тестирует Foo. Вместо написания тестового класса только для Foo, напишите тестовый класс, который совместно проверит два класса.

Итак, если договор Foo должен выполнять X, Y и Z, то ваши тестовые примеры будут делать следующие вещи с помощью FooFactoryImpl.

  • Вызов create и тест, созданный объект делает X.
  • вызов create и тест, созданный объект делает Y.
  • Вызов create и тест, созданный объект делает Z.

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

+0

В сценарии реального мира здесь Foo больше похож на уровень базы данных или веб-сервер, чем на маленький объект, который выполняет три или четыре задания - и имеет связанные зависимости ресурсов. Представьте, что это соединение с базой данных, и одним из заводских методов является 'setPassword'; Я не могу назвать 'getPassword'. Также не имеет смысла видеть, допустил ли я юридическое подключение к базе данных в модульном тесте, где базы данных не существует! Чтобы протестировать созданный объект на основе его поведения, это гораздо более «интеграционный тест», чем я хочу. И он также ожидает, что в библиотеке нет ошибок, что тоже плохо. – durron597

Смежные вопросы