воскресенье, 23 марта 2014 г.

Behavior-based testing with JMockit




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

По-умолчанию для мокируемого типа замокируются все его методы, если тип является классом, то и все методы его родительской цепочки кроме java.lang.Object, при этом все методы любой области видимости и даже конструкторы.

Record, Replay, Verify

Любой тест может быть поделен на три фазы как минимум.
1) Подготовка объектов.
2) Выполнение тестируемого юнита.
3) Проверка полученных результатов.

Эта модель еще называется "три AAA" - Arrange, Act, Assert.

В BBT тестирование это называется "The record-replay-verify model"

В контексте JMockit под эти фазы попадают следующие блоки кода:
Record
  //some not JMockit preparations
  new Expectations(){{}} |  new NonStrictExpectations(){{}}
Replay
  //execution of unit's methods
Verify
  //some Assertions
  new Verifications(){{}} | ...
  //some Assertions

Mocked types

Моками стают фильды тестового класса помеченные одной из 3-4 аннотаций, или параметры методов тестового класса. При этом не обязательно чтобы в тесте пользовались именно этой инстанцией, возможен вариант когда внутри юнита будет создаваться новый объект вызовом конструктора того же типа, для которого мы создали мок. В этом и состоит главное отличие JMockit от традиционных моковских фреймворков - в традиционных мокируются конкретные инстанции, в JMockit - все объекты, создаваемые на любом этапе того типа который помечен аннотацией либо в фильде тестового класса либо в параметре тестового метода.

Expectations подразумевает, что все вызовы записанные в этом блоке, будут неявно проверены вне зависимости от того будут ли они проверятся в блоке Verifications или нет.

Рекорд-блоки могут нам помочь обеспечить возврат нужного в тесте результата из методов моков, для этого есть поле и метод в классах Expectations как:
1. result - поле, определяет конкретное значение, которое вернет вызов метода, после которого этому поле делается присвоение этому значению. Также можно ему присвоить объект класса исключения, что будет означать что при обращению к нему будет кинуто исключение.
2. returns - метод с переменным числом параметров. Означает что в последовательном обращении к методу(в цикле например) будут возвращены указанные значения. Того же эффекта мы достигнем присвоив массив или список полю result.

Подстановка вызовов для конкретных инстанций

Хоть в Expectations блоках мы и определяем роботу как-будто методов конкретной моки, на самом деле мы определяем метод класса, то есть все моки такого типа получают эту реализацию метода - переданные параметром и помеченные @Mocked, или из поля @Mocked тестового класса, и даже созданного в методе тестового класса оператором new.

Для того чтобы завязать реализацию именно на конкретную моку, есть два способа:
1.  Использование обвертки с помощью метода onInstance

   @Test
   public void matchOnMockInstance(@Mocked final Collaborator mock)
   {
      new NonStrictExpectations() {{
         onInstance(mock).getValue(); result = 12;
      }};

      // Exercise unit under test with mocked instance passed from the test:
      int result = mock.getValue();
      assertEquals(12, result);

      // If another instance is created inside code under test...
      Collaborator another = new Collaborator();

      // ...we won't get the recorded result, but the default one:
      assertEquals(0, another.getValue());
   }
2. Использование аннотации @Injectable
Плюс такого метода очень показателен на следующем примере, когда у нас нужно вызвать оригинальный метод одного класса, но за мокать другого, который используется в первом, но оба класса реализуют один интерфейс  и описанные методы - один метод интрефейса:
//Test target
public static final class ConcatenatingInputStream extends InputStream
{
   private final Queue<InputStream> sequentialInputs;
   private InputStream currentInput;

   public ConcatenatingInputStream(InputStream... sequentialInputs)
   {
      this.sequentialInputs = new LinkedList<InputStream>(Arrays.asList(sequentialInputs));
      currentInput = this.sequentialInputs.poll();
   }

   @Override
   public int read() throws IOException
   {
      if (currentInput == null) return -1;

      int nextByte = currentInput.read();

      if (nextByte >= 0) {
         return nextByte;
      }

      currentInput = sequentialInputs.poll();
      return read();
   }
}

...
@Test
public void concatenateInputStreams(
      @Injectable final InputStream input1, @Injectable final InputStream input2) throws Exception {

      new Expectations() {{
         input1.read(); returns(1, 2, -1);
         input2.read(); returns(3, -1);
      }};

      InputStream concatenatedInput = new ConcatenatingInputStream(input1, input2);
      byte[] buf = new byte[3];
      concatenatedInput.read(buf);

      assertArrayEquals(new byte[] {1, 2, 3}, buf);
   }

Гибкая подстановка/ожидание значений аргументов для методов моков 

Все это методы и поля mockit.Invocations. Они подставляются на места аргументов в блоках Exceptions/Verifications

Их можно разбить на классы.

Первый класс методы "with"

(List) withNotNull()
withSameInstance(someInstance)
withSubstring("sub")
withAny(1L)

Кроме уже реализованных можно создавать свои через генерик методы, тут кстати большой помощник библиотека  Hamcrest 

Второй класс поля "any"

(List) any
anyString
anyLong


Чтобы ускорить разработку, если у нас уже использован один из классов подстановок, то дальше мы можем использовать null, который будет означать, что любой допустимый аргумент для метода, если же нам нужен реально null, то тогда withNull()

Иногда нам нужно работать с varargs параметрами в методах или конструкторах для подстановки. В этом случае мы можем ожидать:
1) К конкретным значения
2) набор Any||With
3) для любого количества(0 парметров ключительно) (Object[]) any
Но смешивать конретные и шаблонные мы не можем.

Опередение ограничений на количество вызовов

mocking API предлагает специальных три поля для этого timesminTimes, and maxTimes


@Test
   public void someTestMethod(@Mocked final DependencyAbc abc)
   {
      new Expectations() {{
         // By default, one invocation is expected, i.e. "times = 1":
         new DependencyAbc();

         // At least two invocations are expected:
         abc.voidMethod(); minTimes = 2;

         // 1 to 5 invocations are expected:
         abc.stringReturningMethod(); minTimes = 1; maxTimes = 5;
      }};

      new UnitUnderTest().doSomething();
   }

   @Test
   public void someOtherTestMethod(@Mocked final DependencyAbc abc)
   {
      new UnitUnderTest().doSomething();

      new Verifications() {{
         // Verifies that zero or one invocations occurred, with the specified argument value:
         abc.anotherVoidMethod(3); maxTimes = 1;

         // Verifies the occurrence of at least one invocation with the specified arguments:
         DependencyAbc.someStaticMethod("test", false); // "minTimes = 1" is implied
      }};
   }

Процесс верификации. Поехали

Строгие ожидания подразумевают неявную верификацию. А вот без них мы верифицируем явно. Это касается как NonStrictExpectation, так и вообще вариантов теста без рекорд блока.


   @Test
   public void verifyInvocationsExplicitlyAtEndOfTest(@Mocked final Dependency mock)
   {
      // Nothing recorded here, though it could be.

      // Inside tested code:
      Dependency dependency = new Dependency();
      dependency.doSomething(123, true, "abc-xyz");

      // Verifies that Dependency#doSomething(int, boolean, String) was called at least once,
      // with arguments that obey the specified constraints:
      new Verifications() {{ mock.doSomething(anyInt, true, withPrefix("abc")); }};
   }
По-умолчанию блок верификации Verifications проверяет наличие как минимум одного указанного вызова, если нужно конкретное количество, то нужно явно использовать times = n ограничение.
Чтобы точно не было инвокации - times = 0 
Чтобы порядок вызовов тоже верифицировался, нужно использовать VerificationsInOrder

   @Test
   public void verifyingExpectationsInOrder(@Mocked final DependencyAbc abc)
   {
      // Somewhere inside the tested code:
      abc.aMethod();
      abc.doSomething("blah", 123);
      abc.anotherMethod(5);
      ...

      new VerificationsInOrder() {{
         // The order of these invocations must be the same as the order
         // of occurrence during replay of the matching invocations.
         abc.aMethod();
         abc.anotherMethod(anyInt);
      }};
   }

Мы также можем писать частично упорядоченные верификации. Делаются они в VerificationsInOrder блоке, на местах где нас не интересует порядок вызовов каких-то методов, и каких конкретно нам все равно, мы ставим вызов unverifiedInvocations(); в

   @Mocked DependencyAbc abc;
   @Mocked AnotherDependency xyz;

   @Test
   public void verifyingTheOrderOfSomeExpectationsRelativeToAllOthers()
   {
      new UnitUnderTest().doSomething();

      new VerificationsInOrder() {{
         abc.methodThatNeedsToExecuteFirst();
         unverifiedInvocations(); // Invocations not verified must come here...
         xyz.method1();
         abc.method2();
         unverifiedInvocations(); // ... and/or here.
         xyz.methodThatNeedsToExecuteLast();
      }};
   }
Для варианта когда нам нужно четко также определить, что конкретные методы на месте unverifiedInvocations(); будут выполнены, нам нужно комбинировать оба блока как VerificationsInOrder, так и Verifications


   @Test
   public void verifyFirstAndLastCallsWithOthersInBetweenInAnyOrder()
   {
      // Invocations that occur while exercising the code under test:
      mock.prepare();
      mock.setSomethingElse("anotherValue");
      mock.setSomething(123);
      mock.notifyBeforeSave();
      mock.save();

      new VerificationsInOrder() {{
         mock.prepare(); // first expected call
         unverifiedInvocations(); // others at this point
         mock.notifyBeforeSave(); // just before last
         mock.save(); times = 1; // last expected call
      }};

      // Unordered verification of the invocations previously left unverified.
      // Could be ordered, but then it would be simpler to just include these invocations
      // in the previous block, at the place where "unverifiedInvocations()" is called.
      new Verifications() {{
         mock.setSomething(123);
         mock.setSomethingElse(anyString);
      }};
   }
При этом порядок в котором идут VerificationsInOrder и Verifications важен, если мы поменяем, например, в этом тесте их местами, то он не сломается. Но вот что вызовы mock.setSomething(123); mock.setSomethingElse(anyString); могут быть сделаны до mock.prepare();, а под блок unverifiedInvocations(); просто ничего не попадет. Вот и попались...

Когда нам нужно убедиться, что не осталось ни одного вызова без проверки, мы используем
FullVerifications. Это аналог но с другой стороны строго Expectations
@Test
   public void verifyAllInvocations(@Mocked final Dependency mock)
   {
      // Code under test included here for easy reference:
      mock.setSomething(123);
      mock.setSomethingElse("anotherValue");
      mock.setSomething(45);
      mock.save();

      new FullVerifications() {{
         // Verifications here are unordered, so the following invocations could be in any order.
         mock.setSomething(anyInt); // verifies two actual invocations
         mock.setSomethingElse(anyString);
         mock.save(); // if this verification (or any other above) is removed the test will fail
      }};
   }
mock.setSomething(anyInt) удовлетворяет оба вызова, потому что FullVerifications не упорядоченная верификация и любая проверка означает "минимум один вызов".

Если нужно упорядоченная то:
   @Test
   public void verifyAllInvocationsInOrder(@Mocked final Dependency mock)
   {
      // Code under test included here for easy reference:
      mock.setSomething(123);
      mock.setSomethingElse("anotherValue");
      mock.setSomething(45);
      mock.save();

      new FullVerificationsInOrder() {{
         mock.setSomething(anyInt);
         mock.setSomethingElse(anyString);
         mock.setSomething(anyInt);
         mock.save();
      }};
   }
Тут уже видно, что мы обязаны определять mock.setSomething(anyInt); дважды по очевидной причине. Может показаться, что мы замкнули цикл и имеем прямой, но с другой стороны аналог строгого Expectations.  Но это не так если на одном из мест FullVerificationsInOrder методы будут вызываться по несколько раз, в таком случае все сработает. Чтобы был полный аналог, нам нужно ставить times = 1;

Иногда бывает, что нам нужно четко проверять один мок, а другие не нужно, для этого мока вяжется к блоку:
   @Test
   public void verifyAllInvocationsToOnlyOneOfTwoMockedTypes(
      @Mocked final Dependency mock1, @Mocked AnotherDependency mock2)
   {
      // Inside code under test:
      mock1.prepare();
      mock1.setSomething(123);
      mock2.doSomething();
      mock1.editABunchMoreStuff();
      mock1.save();

      new FullVerifications(mock1) {{
         mock1.prepare();
         mock1.setSomething(anyInt);
         mock1.editABunchMoreStuff();
         mock1.save(); times = 1;
      }};
   }
Так передавая моки в конструктор классов FullVerifications(...), FullVerificationsInOrder(...), строгость проверки мы цепляем только на конкретные моки.

Можно также убедиться, что конкретная мока не будет дернута:
   @Test
   public void verifyNoInvocationsOnOneOfTwoMockedDependenciesBeyondThoseRecordedAsExpected(
      @Mocked final Dependency mock1, @Mocked final AnotherDependency mock2)
   {
      new NonStrictExpectations() {{
         // These two are recorded as expected:
         mock1.setSomething(anyInt); minTimes = 1;
         mock2.doSomething(); times = 1;
      }};

      // Inside code under test:
      mock1.prepare();
      mock1.setSomething(1);
      mock1.setSomething(2);
      mock1.save();
      mock2.doSomething();

      // Will verify that no invocations other than to "doSomething()" occurred on mock2:
      new FullVerifications(mock2) {};
   }

Делегирование, горячее формирование ответов из моков, основанное на фактических входных параметрах

   @Test
   public void someTestMethod(@Mocked final DependencyAbc abc)
   {
      new NonStrictExpectations() {{
         abc.intReturningMethod(anyInt, null);
         result = new Delegate() {
            int aDelegateMethod(int i, String s)
            {
               return i == 1 ? i : s.length();
            }
         };
      }};

      new UnitUnderTest().doSomething();
   }
mockit.Delegate пустой интерфейс, реализуя его мы можем установить ему метод с любым названием, которые будет реализацией указанного выше метода мока. Полезно первым аргументом такого метода ставить объект  Invocation, из него мы можем получить реальные параметры, ссылку на объект моки. Также мы можем определить реальные параметры и просто пользоваться ими, если нам не нужна ссылка на саму инстанцию моки.

Также мы можем делегировать конструктора:
   @Test
   public void delegatingConstructorInvocations(@Mocked Collaborator mock)
   {
      new Expectations() {{
         new Collaborator(anyInt);
         result = new Delegate() {
            void delegate(int i) { if (i < 1) throw new IllegalArgumentException(); }
         };
      }};

      new Collaborator(4);
   }

Запоминание вызываемых аргументов для дальнейших проверок

 special "withCapture(...)" methods
   @Test
   public void capturingArgumentsFromSingleInvocation(@Mocked final Collaborator mock)
   {
      // Inside tested code:
      new Collaborator().doSomething(0.5, new int[2], "test");

      new Verifications() {{
         double d;
         String s;
         mock.doSomething(d = withCapture(), null, s = withCapture());

         assertTrue(d > 0.0);
         assertTrue(s.length() > 1);
      }};
   }
Теперь мы можем применить более изощренную проверку.

Также мы можем получить список вызванных параметров, если проходит несколько обращений к мокируемому методу.
   @Test
   public void capturingArgumentsFromMultipleInvocations(@Mocked final Collaborator mock)
   {
      mock.doSomething(dataObject1);
      mock.doSomething(dataObject2);

      new Verifications() {{
         List<Dataobject> dataObjects = new ArrayList<>();
         mock.doSomething(withCapture(dataObjects));

         assertEquals(2, dataObjects.size());
         DataObject data1 = dataObjects.get(0);
         DataObject data2 = dataObjects.get(1);
         // Perform arbitrary assertions on data1 and data2.
      }};
   }

Доступ к приватным полям, методам и конструкторам

mockit.Deencapsulation использует рефлексию. В следующем примере мы будем использовать его методы setField, newInstance, invoke, getField
   import static mockit.Deencapsulation.*;

   @Test
   public void someTestMethod(@Mocked final DependencyAbc abc)
   {
      final UnitUnderTest tested = new UnitUnderTest();

      // Defines some necessary state on the unit under test:
      setField(tested, "someIntField", 123);

      new NonStrictExpectations() {{
         // Expectations still recorded, even if the invocations are done through Reflection:
         newInstance("some.package.AnotherDependency", true, "test"); maxTimes = 1;
         invoke(abc, "intReturningMethod", 45, ""); result = 1;
         // other expectations recorded...
      }};

      tested.doSomething();

      String result = getField(tested, "result");
      assertEquals("expected result", result);
   }
Статические методы мы можем вызывать в любом месте, это нам позволяет пользоваться этим как при записе моков, так и при верификации каких-то промежуточных результатов, или состояний обьектов.

Частичное мокирование

По-умолчанию все методы мокированного типа мокированы, это по большому счету устраивает 95% все случаев, но иногда бывает необходимость замокировать некоторые методы, а в остальных дергать реальные реализации.

Статическое частичное мокирование

Может быть явно определено через атрибут аннотации @Mocked(value = {"filter1", "filter2", ...}) Каждое значение фильтра можемт быть определено через регулярку, чтобы под нее подпадала группа методов.
public class MyTestClass
{
   @Mocked("nanoTime") final System system = null;

   @Test
   public void staticPartialMocking(
      @Mocked({"(int)", "doInternal()", "[gs]etValue", "complexOperation(Object)"})
      final Collaborator mock)
   {
      ...
      new NonStrictExpectations() {{
         mock.getValue(); result = 123;
      }};
      ...
   }
}

Динамическое частичное мокирование

Статическое частичное не такое удобное как хотелось бы, нам нужно явно определять какие методы/конструкторы будут мокироваться, да еще и в стороках, а не в коде, все это влечет за собой много лишнего, что не дружит с рефакторингом.
Простой и если задуматься очевидный способ, позволить JMockit самому решить, что ему использовать на Replay phase, толи реальрую реализацию, толи мок, а это просто - все что было записано на предыдущей фазе вызывается, все что нет - используется оригинал. И это все решается в рантайме
public final class DynamicPartialMockingTest
{
   static class Collaborator
   {
      private final int value;

      Collaborator() { value = -1; }
      Collaborator(int value) { this.value = value; }

      int getValue() { return value; }
      final boolean simpleOperation(int a, String b, Date c) { return true; }
      static void doSomething(boolean b, String s) { throw new IllegalStateException(); }
   }

   @Test
   public void dynamicallyMockAClass()
   {
      new Expectations(Collaborator.class) {{
         new Collaborator().getValue(); result = 123;
      }};

      // Mocked:
      Collaborator collaborator = new Collaborator();
      assertEquals(123, collaborator.getValue());

      // Not mocked:
      assertTrue(collaborator.simpleOperation(1, "b", null));
      assertEquals(45, new Collaborator(45).value);
   }

   @Test
   public void dynamicallyMockAnInstance()
   {
      final Collaborator collaborator = new Collaborator(2);

      new NonStrictExpectations(collaborator) {{
         collaborator.simpleOperation(1, "", null); result = false;
         Collaborator.doSomething(anyBoolean, "test");
      }};

      // Mocked:
      assertFalse(collaborator.simpleOperation(1, "", null));
      Collaborator.doSomething(true, "test");

      // Not mocked:
      assertEquals(2, collaborator.getValue());
      assertEquals(45, new Collaborator(45).value);
      assertEquals(-1, new Collaborator().value);
   }
}
Ключ к пониманию состоит в том, что у нас нет переданных моков помеченных аннотацией @Mocked, по-этому в Expectations(Object...) and NonStrictExpectations(Object...) мы передаем либо классы, частичные моки для которых необходимо сделать, либо даже обьекты, мокируем мы то, что переписываем в Expectations блоках. Для класса мокироваться будут только его прямые методы, не его предков, а в обьектах мы можем любые только не рут отца Object.
Еще прикольная фишка, если в строгом Expectations мы пользуюясь times or maxTimes , или не пользуясь а положившись на дефолтный 1 раз, мы переопределим количество вызовов, то если мы продолжим дергать указанный метод, исключения не будет, а бедут уже использоваться оригинальная реализация класса.

Каскадное мокирование

Когда ипользуется сложное API длинными цепочками вызовов никого не удивиш obj1.getObj2(...).getYetAnotherObj().doSomething(...). Именно для таких моков испльзуется аннотация @Cascading, которая как и @Mocked может быть только применена к полям и параметрам класса-теста. Аннотация обеспечивает, что любое обращение к обьекту, к которому по цепочке зависимостей от класса-мока, никогда не свалится с налпоинтером, потому что все такие обращения на любую глубину тоже будут замокированны.
   @Test
   public void recordAndVerifyExpectationsOnCascadedMocks(
      @Cascading final Socket mock, @Mocked InetSocketAddress unused)
      throws Exception
   {
      new NonStrictExpectations() {{
         mock.getChannel().isConnected(); result = false;
      }};

      // Inside production code:
      Socket s = new Socket(...);
      ...
   
      if (!s.getChannel().isConnected()) {
         SocketAddress sa = new InetSocketAddress("remoteHost", 123);
         s.getChannel().connect(sa);
      }

      ...
      new Verifications() {{
         mock.getChannel().connect((SocketAddress) withNotNull());
      }};
   }

Если на какой-то цепочки зависимости нашей моки, нам нужна конкретная реализация моки, мы можем ее туда присвоит в фазе записи с помощью result field - просто и очевидно.

"Собирательство"

Бывают ситуации, когда в тестируемом юните много анонимных реализаций интерфейсов или анонимного расширения классов. К ним кроме как рефлексией ни как и не добраться.
Именно для этого и придумали "собирательство".
Мы указываем какие типы нам нужно собирать либо в поле тестового класса, либо в параметре тестового метода. И каждый раз когда в юните будет создаваться реализация указанного типа, на это место будет ставиться мок, а что с ним будет потом происходить, мы можем указать в блоке записи, или проверить в блоке проверки. Делается это аннотацией @Capturing , также в ней есть опциональный атрибут maxInstances  который определяет сколько моков будет созданно, а потом будут создаваться оригинальные реализации, если атрибут не указан - моки создаются для всех реализаций.
Вот пример
public interface Service { int doSomething(); }
final class ServiceImpl implements Service { public int doSomething() { return 1; } }

public final class TestedUnit
{
   private final Service service1 = new ServiceImpl();
   private final Service service2 = new Service() { public int doSomething() { return 2; } };
   Observable observable;

   public int businessOperation(final boolean b)
   {
      new Callable() { // Callable is a parameterized interface from java.util.concurrent
         public Object call() { throw new IllegalStateException(); }
      }.call();

      observable = new Observable() {{ // Observable is a concrete class from java.util
         if (b) {
            throw new IllegalArgumentException();
         }
      }};

      return service1.doSomething() + service2.doSomething();
   }
}

public final class UnitTest
{
   @Capturing Service service;

   @Test
   public void captureInternallyCreatedInstances(
      @Capturing(maxInstances = 1) final Callable callable, @Capturing Observable observable)
      throws Exception
   {
      new NonStrictExpectations() {{
         service.doSomething(); returns(3, 4);
      }};

      TestedUnit unit = new TestedUnit();
      int result = unit.businessOperation(true);

      assertEquals(7, result);
      assertNotNull(unit.observable);

      new Verifications() {{ callable.call(); }};
   }
}

Автоматическое создание инстанции и вприскивание зависимостей в тестируемые классы

Чаще всего мы тестируем в отдельном тесте один класс из юнита. Аннотация  @Tested  помогает нам подготовить тестируемый класс.
Помеченное ею поле в классе-тесте получает инстанцию указанного класса, а все ее зависимости заполняются моками, которые мы тоже помечаем в классе-тесте. Для заполнения зависимостей используются только моки, которые помечены аннотацией @Injectable. Так по количеству и по типам таких мок подискивается подходящий конструктор тестируемого класса, все остальные моки, которые оказались лишними движок попытается впрыснуть через сетеры. Если под сеттер попадает несколько мок с одинаковым типом, движок попытается подобрать более подходящий по имени.
@Injectable могут быть не только классы, но и простые типы, если мы не создаем инстанцию класса или простого типа, то движок создаст мок, а так в тестируемый класс будет вставлена наша реализация, а не мок.
Есть проблемка, когда мы в нескольких методах теста хотим снабдить разными реализациями зависимостей тестируемый класс, в этом случае мы будем пользоваться параметрами тестового метода, а там мы реализацию не можем указать, для этого для простых типов мы можем воспользоваться атрибутом велью аннотации @Injectable 
public class SomeTest
{
   @Tested CodeUnderTest tested;
   @Injectable Dependency dep1;
   @Injectable AnotherDependency dep2;
   @Injectable int someIntegralProperty = 123;

   @Test
   public void someTestMethod(@Injectable("true") boolean flag, @Injectable("Mary") String name)
   {
      // Record expectations on mocked types, if needed.

      tested.exerciseCodeUnderTest();

      // Verify expectations on mocked types, if required.
   }
}

Повторное использование експектейшин и верификейшин блоков

Воздавая моки в тестовом классе, записи делая в @Before методах, а также использую утилитные методы тестового класса:
public final class LoginServiceTest
{
   @Tested LoginService service;
   @Mocked UserAccount account;

   @Before
   public void init()
   {
      new NonStrictExpectations() {{ UserAccount.find("john"); result = account; }};
   }

   @Test
   public void setAccountToLoggedInWhenPasswordMatches() throws Exception
   {
      willMatchPassword(true);

      service.login("john", "password");

      new Verifications() {{ account.setLoggedIn(true); }};
   }

   private void willMatchPassword(final boolean match)
   {
      new NonStrictExpectations() {{ account.passwordMatches(anyString); result = match; }};
   }

   @Test
   public void notSetAccountLoggedInIfPasswordDoesNotMatch() throws Exception
   {
      willMatchPassword(false);

      service.login("john", "password");

      new Verifications() {{ account.setLoggedIn(true); times = 0; }};
   }

   // other tests that use the "account" mock field
}
Еще один вариант определять наследников Expectations|Verifications классов, либо как внутренними тестового классами либо даже топ-уровневыми(если его можно использовать в нескольких тестовых классах), так мы можем создавать неаннонимные классы.
   final class PasswordMatchingExpectations extends NonStrictExpectations
   {
      PasswordMatchingExpectations(boolean match)
      {
         account.passwordMatches(anyString); result = match;
      }
   }

   @Test
   public void setAccountToLoggedInWhenPasswordMatches() throws Exception
   {
      new PasswordMatchingExpectations(true);

      ...
   }
Только вот нужно чтобы обязательно такие наследники имели в своих именах постфиксы Expectations|Verifications, потому что JMockit их по другому не найдет.

Мокирование будущей реализации, которая реализует несколько интрейфейсов

Мы знаем, что у нас будет такая реализация, но пока ее нет, что же с помощью джененриков мы можем определить себе такой тип и создать на него мок:
public interface Dependency // an arbitrary custom interface
{
   String doSomething(boolean b);
}

public final class MultiMocksTest
{
   @Mocked MultiMock multiMock;

   @Test
   public void mockFieldWithTwoInterfaces()
   {
      new NonStrictExpectations() {{
         multiMock.doSomething(false); result = "test";
      }};

      multiMock.run();
      assertEquals("test", multiMock.doSomething(false));

      new Verifications() {{ multiMock.run(); }};
   }

   @Test
   public  void mockParameterWithTwoInterfaces(
      @Mocked final M mock)
   {
      new Expectations() {{
         mock.doSomething(true); result = ""
      }};

      assertEquals("", mock.doSomething(true));
   }
}

Повторяющиеся ожидания

Если у нас будут какие-то циклы на этапе воспроизведения, то удобно на этапе записи не заниматься копи-пейстом, для это мы можем передать целое число в контруктор Expectations блока:
   @Test
   public void recordStrictInvocationsInIteratingBlock(@Mocked final Collaborator mock)
   {
      new Expectations(2) {{
         mock.setSomething(anyInt);
         mock.save();
      }};

      // In the tested code:
      mock.setSomething(123);
      mock.save();
      mock.setSomething(45);
      mock.save();
   }

Проверка повторений

Теперь нам было бы не плохо проверить повторения. И таки да - есть и в Verifications блоках конструкторы с количеством повторений проверки.
   @Test
   public void verifyAllInvocationsInLoop(@Mocked final Dependency mock)
   {
      int numberOfIterations = 3;

      // Code under test included here for easy reference:
      for (int i = 0; i < numberOfIterations; i++) {
         DataItem data = getData(i);
         mock.setData(data);
         mock.save();
      }

      new Verifications(numberOfIterations) {{
         mock.setData((DataItem) withNotNull());
         mock.save();
      }};

      new VerificationsInOrder(numberOfIterations) {{
         mock.setData((DataItem) withNotNull());
         mock.save();
      }};
   }
Тут специально преведено два типа, чтобы сконцентрироваться на разнице в их работе. Первый вариант : в нем не важен порядок, в нем просто минимальная и максимальная граница умножаются на целое из контруктора -- вхождение в эти рамки обеспечивает прохождение проверки, если у нас допусти явно поставлено где-то minTimes = 1; maxTimes = 4;, то в данном примере это будет minTimes = 3; maxTimes = 12;
Второй же блок можно бы назвать блоком разворачивания -- тоесть это тоже самое, что прокопипейстить содержимое инит-блока.
FullVerifications блок соответствует "умножению", блок FullVerificationsInOrder  -- развертыванию.

Комментариев нет:

Отправить комментарий