2017-06-18 36 views
5

Dodano @VisibleForTesting i chroniony. Mój test może teraz ta metoda:Testowanie klasy Fragment w izolacji za pomocą Mockito

@VisibleForTesting 
    protected void setupDataBinding(List<Recipe> recipeList) { 
     recipeAdapter = new RecipeAdapter(recipeList); 
     RecyclerView.LayoutManager layoutManager 
       = new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false); 
     rvRecipeList.setLayoutManager(layoutManager); 
     rvRecipeList.setAdapter(recipeAdapter); 
    } 

Updated przypadek testowy przy użyciu obiekt szpiegowski: Jednak prawdziwym setupDataBinding (przepis) nazywana jest uzyskiwanie nawet kiedy stworzyliśmy makietę He szpiegować, że będzie się nazywa. Może robię to źle.

@Test 
public void testShouldGetAllRecipes() { 
    RecipeListView spy = Mockito.spy(fragment); 
    doNothing().when(spy).setupDataBinding(recipe); 

    fragment.displayRecipeData(recipe); 

    verify(recipeItemClickListener, times(1)).onRecipeItemClick(); 
} 

Próbuję przetestować metody w mojej klasie Fragment, jak poniżej. Próbuję jednak wyśmiewać metody sprawdzania, czy metody są nazywane odpowiednią liczbą razy. Jednak problem polega na tym, że mam ustawienia private, które są konfigurowane na RecyclerView, które są wywoływane z displayRecipeData(...). Chcę udawać te połączenia, ponieważ nie chcę wywoływać prawdziwego obiektu na RecyclerView. Chcę tylko sprawdzić, czy zostanie wywołana setupDataBinding(...).

Próbowałem używać szpiega i VisibleForTesting, ale wciąż nie jestem pewien, jak to zrobić.

Próbuję przetestować Fragment w izolacji.

public class RecipeListView 
     extends MvpFragment<RecipeListViewContract, RecipeListPresenterImp> 
     implements RecipeListViewContract { 

    @VisibleForTesting 
    private void setupDataBinding(List<Recipe> recipeList) { 
     recipeAdapter = new RecipeAdapter(recipeList); 
     RecyclerView.LayoutManager layoutManager 
       = new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false); 
     rvRecipeList.setLayoutManager(layoutManager); 
     rvRecipeList.setAdapter(recipeAdapter); 
    } 

    @Override 
    public void displayRecipeData(List<Recipe> recipeList) { 
     /* Verify this get called only once */ 
     setupDataBinding(recipeList); 

     recipeItemListener.onRecipeItem(); 
    } 
} 

Tak właśnie testuję. Dodałem myślenie VisibleForTesting, które mogłem pomóc. Próbowałem też użyć szpiega.

public class RecipeListViewTest { 
    private RecipeListView fragment; 
    @Mock RecipeListPresenterContract presenter; 
    @Mock RecipeItemListener recipeItemListener; 
    @Mock List<Recipe> recipe; 

    @Before 
    public void setup() { 
     MockitoAnnotations.initMocks(RecipeListViewTest.this); 
     fragment = RecipeListView.newInstance(); 
    } 

    @Test 
    public void testShouldGetAllRecipes() { 
     fragment.displayRecipeData(recipe); 
     RecipeListView spy = Mockito.spy(fragment); 

     verify(recipeItemListener, times(1)).onRecipeItem(); 
    } 
} 

Jaki byłby najlepszy sposób, aby przetestować powyższe w izolacji?

Wielkie dzięki za radę.

+0

Dodawanie '@ VisibleForTesting' nie wystarczy. Musisz również zmienić modyfikator dostępu dla 'setupDataBinding (...)' na chroniony, pakiet prywatny lub publiczny. – liminal

+0

@liminal Zaktualizowałem moje pytanie z moimi ostatnimi uwagami. Nie mogę zapobiec wywołaniu prawdziwej metody bing, mimo że stworzyłem jej obiekt szpiegowski. – ant2009

Odpowiedz

4

aby zapobiec rzeczywisty sposób miano zastosowanie: Mockito.doNothing().when(spy).onRecipeItem();

tutaj masz minimalną próbkę jak go używać:

public class ExampleUnitTest { 
    @Test 
    public void testSpyObject() throws Exception { 
     SpyTestObject spyTestObject = new SpyTestObject(); 
     SpyTestObject spy = Mockito.spy(spyTestObject); 

     Mockito.doNothing().when(spy).methodB(); 

     spy.methodA(); 
     Mockito.verify(spy).methodB(); 
    } 

    public class SpyTestObject { 

     public void methodA() { 
      methodB(); 
     } 
     public void methodB() { 
      throw new RuntimeException(); 
     } 
    } 

}

3

chcę wyśmiewać tych połączeń jak i don nie chcesz wywoływać prawdziwego obiektu na RecyclerView. Chcę tylko sprawdzić, czy zostanie wywołana setupDataBinding().

Nie stworzyłeś wystarczającej ilości szwów, aby to wykonać.

Co się stanie, jeśli zadeklarujesz umowę, która opisuje, w jaki sposób nastąpi "powiązanie danych konfiguracji"? Innymi słowy, co jeśli stworzysz interfejs z metodą void setupDataBinding(...)? Następnie RecipeListView utrzyma instancję tego interfejsu jako zależność. Tak więc, RecipeListView nigdy się nie dowie, jak dokładnie to się odbędzie: jedna rzecz jaką wie - zależność, którą posiada, "podpisała kontrakt" i wzięła na siebie odpowiedzialność za wykonanie pracy.

Normalnie byś przekazać tę zależność za pomocą konstruktora, ale ponieważ Fragment is a specific case, można nabyć w zależności onAttach():

interface Setupper { 
    void setupDataBinding(List<Recipe> recipes, ...); 
} 

class RecipeListView extends ... { 

    Setupper setupper; 

    @Override public void onAttach(Context context) { 
     super.onAttach(context); 

     // Better let the Dependency Injection tool (e.g. Dagger) provide the `Setupper` 
     // Or initialize it here (which is not recommended) 
     Setupper temp = ... 
     initSetupper(temp); 
    } 

    void initSetupper(Setupper setupper) { 
     this.setupper = setupper; 
    } 

    @Override 
    public void displayRecipeData(List<Recipe> recipes) { 
     // `RecipeListView` doesn't know what exactly `Setupper` does 
     // it just delegates the work 
     setupper.setupDataBinding(recipes, ...); 

     recipeItemListener.onRecipeItem(); 
    } 
} 

Co to daję wam? Teraz masz szew.Teraz jesteś nie w zależności od realizacji, jesteś zależny od umowy.

public class RecipeListViewTest { 

    @Mock Setupper setupper; 
    List<Recipe> recipe = ...; // initialize, no need to mock it 
    ... 

    private RecipeListView fragment; 

    @Before 
    public void setup() { 
     MockitoAnnotations.initMocks(this); 
     fragment = new RecipeListView(); 
     fragment.initSetupper(setupper); 
    } 

    @Test 
    public void testShouldGetAllRecipes() { 
     fragment.displayRecipeData(recipes); 

     // You do not care what happens behind this call 
     // The only thing you care - is to test whether is has been executed 
     verify(setupper).setupDataBinding(recipe, ...); 
     // verify(..) is the same as verify(.., times(1)) 
    } 
} 

Zdecydowanie poradę Misko Hevery jest "Writing Testable Code" książki, która przedstawia wszystkie techniki, przykłady i w sposób zwięzły (38 stron).

1

Istnieje wspólna zasada mówiąca: Znacznie lepiej jest sprawdzić, co jednostka robi, a nie jak to robi.

Biorąc to pod uwagę, należy zadać sobie pytanie - dlaczego miałbym drwić setupDataBinding metody w pierwszej kolejności? Nie wykonuje żadnych połączeń zewnętrznych, zmienia jedynie stan obiektu. Tak jest lepszy sposób, aby przetestować ten kod jest sprawdzenie, czy to zmienia stan w prawidłowy sposób:

@Test 
public void testShouldGetAllRecipes() { 
    fragment.displayRecipeData(recipeList); 

    // Verifies whether RecipeAdapter has been initialized correctly 
    RecipeAdapter recipeAdapter = fragment.getRecipeAdapter(); 
    assertNotNull(recipeAdapter); 
    assertSame(recipeList, recipeAdapter.getRecipeList()); 

    // Verifies whethr RvRecipeList has been initialized correctly 
    RvRecipeList rvRecipeList = fragment.getRvRecipeList(); 
    assertNotNull(rvRecipeList); 
    assertNotNull(rvRecipeList.getLayoutManager()); 
    assertSame(fragment.getRecipeAdapter(), rvRecipeList.getAdapter()); 
} 

Może to wymagać dodanie kilku pobierające/ustawiaczy, aby cała sprawa nieco bardziej sprawdzalne.

+0

Misko Hevery: "Zwykle adnotacja @VisibleForTesting jest zapachem, którego klasa nie została napisana, aby być łatwo przetestowaną, i nawet jeśli pozwoli ci ustawić listę połączeń, to jest tylko hack, aby uzyskać około korzeń problem." – azizbekian

+0

Tak, zgadzam się. Cóż, wystarczy, że zdobycze i setery będą wystarczające. Zaktualizowałem swoją odpowiedź. –