Mockito: внедрение макетов в поток управления

Я все еще изучаю мокито, и сейчас я учусь вводить моки.

У меня есть тестируемый объект с определенным методом, который зависит от других объектов. Эти объекты, в свою очередь, зависят от других объектов. Я хочу издеваться над определенными вещами и использовать эти моки везде во время выполнения — на протяжении всего потока управления методом.

Например, предположим, что есть такие классы, как:

public class GroceryStore {
    public double inventoryValue = 0.0;
    private shelf = new Shelf(5);
    public void takeInventory() {
        for(Item item : shelf) {
            inventoryValue += item.price();
        }
    }
}

public class Shelf extends ArrayList<Item> {
    private ProductManager manager = new ProductManager();
    public Shelf(int aisleNumber){
        super(manager.getShelfContents(aisleNumber);
    }
}

public class ProductManager {
    private Apple apple;
    public void setApple(Apple newApple) {
        apple = newApple;
    }
    public Collection<Item> getShelfContents(int aisleNumber) {
        return Arrays.asList(apple, apple, apple, apple, apple);
    }
}

Мне нужно написать тестовый код с частями по строкам:

....
@Mock
private Apple apple;
... 
when(apple.price()).thenReturn(10.0);
... 

...
@InjectMocks
private GroceryStore store = new GroceryStore();
...
@Test
public void testTakeInventory() {
   store.takeInventory();
   assertEquals(50.0, store.inventoryValue);
}

Всякий раз, когда вызывается apple.price(), я хочу, чтобы использовалось мое фиктивное яблоко. Это возможно?

РЕДАКТИРОВАТЬ:
Важное примечание...
класс, содержащий объект, который я хочу смоделировать, имеет установщик для этого объекта. Однако на самом деле у меня нет доступа к этому классу на том уровне, который я тестирую. Итак, следуя примеру, хотя ProductManager имеет установщик для Apple, у меня нет способа получить ProductManager из объекта GroceryStore.


person gMale    schedule 15.10.2010    source источник
comment
Я думаю, что вы должны создать фабрику для Apple, а затем издеваться над фабрикой.   -  person Alois Cochard    schedule 15.10.2010
comment
@Alois: что-то в этом роде может быть правильным, но . . . как заставить ProductManager использовать фабрику (из моего модульного теста GroceryStore)?   -  person gMale    schedule 15.10.2010
comment
с помощью установщика в ProductManager для определения factory. Используете ли вы какую-либо структуру DI (внедрение зависимостей)? весна или guice например   -  person Alois Cochard    schedule 15.10.2010
comment
@Алоис: Да. Я использую Весну. Этот точный объект загружается через spring. Есть ли способ вместо этого внедрить мой фиктивный объект во время тестирования?   -  person gMale    schedule 15.10.2010


Ответы (1)


Проблема в том, что вы создаете объекты, от которых зависите, вызывая new вместо его внедрения. Вставьте ProductManager в Shelf (например, в конструкторе) и введите Shelf в GroceryStore. Затем в тесте используйте макеты. Если вы хотите использовать @InjectMocks, вам нужно ввести методы установки.

По конструктору это может выглядеть так:

public class GroceryStore {
  public double inventoryValue = 0.0;
  private shelf;

  public GroceryStore(Shelf shelf) {
    this.shelf = shelf;
  }

  public void takeInventory() {
    for(Item item : shelf) {
      inventoryValue += item.price();
    }
  }
}

public class Shelf extends ArrayList<Item> {
  private ProductManager manager;

  public Shelf(int aisleNumber, ProductManager manager) {
    super(manager.getShelfContents(aisleNumber);
    this.manager = manager;
  }
}

public class ProductManager {
  private Apple apple;
  public void setApple(Apple newApple) {
    apple = newApple;
  }
  public Collection<Item> getShelfContents(int aisleNumber) {
    return Arrays.asList(apple, apple, apple, apple, apple);
  }
}

Затем вы можете протестировать его, издеваясь над всеми объектами, от которых вы зависите:

@Mock
private Apple apple;
... 
when(apple.price()).thenReturn(10.0);

@InjectMocks
private ProductManager manager = new ProductManager();

private Shelf shelf = new Shelf(5, manager);
private GroceryStore store = new GroceryStore(shelf);

//Then you can test your store.
person amorfis    schedule 03.11.2010
comment
Я забыл, что у меня был этот вопрос открытым! :) Основной ответ оказался Нет, вы не можете внедрять моки на всем протяжении потока управления вызова метода. Это означает, что вы не можете создать макет и автоматически применять его везде. Вы должны вручную установить его, где вы хотите, так сказать. Гораздо удобнее было бы требовать, везде, где увидишь яблоко, замени его моим чучелом! Но это невозможно. Вы правы, единственное решение - изменить код и внедрить макеты вручную. Спасибо что нашли время ответить. - person gMale; 03.11.2010
comment
Пожалуйста. Обычно хорошей практикой является внедрение зависимостей вместо их создания с помощью new. Это делает ваш код более тестируемым. У Миско Хевери есть хороший путеводитель по этому поводу: .com/code-reviewers-guide/ - person amorfis; 03.11.2010
comment
@gmale: На самом деле, вы можете это сделать (т. е. имитировать все экземпляры данного класса, независимо от того, кто их создает, где и когда), но для этого требуется более мощный инструмент для имитации, такой как JMockit (мой собственный) или PowerMockito. . - person Rogério; 08.12.2010