Lab 3

Lab 3 SVV

Mock Unit Testing

Pasul 1

Descarcati archiva Attach:MockUnit3.zip, apoi deschideti Eclipse, inchideti proiectele curente si importati proiectul descarcat dupa cum urmeaza - mergeti la meniul File : alegeti optiunea Import... > Existing Projects into workspace > Select archive file > Browse > alegeti arhiva proaspat descarcata MockUnit3.zip > Finish

Scrieti apoi teste unitare pentru cazurile de test de mai jos:

 0 tags -> ZeroTag.html 
1 tag -> OneTag.html
2 tags -> TwoTags.html
0 tags -> Empty.html

Pasul 2 - Mocking

In clasa TagsParser adaugati methoda de mai jos, cu scopul de a folosi pattern-ul dependency injection.

  1 public void setProvider(HttpStringProvider p) {
  2         this.provider = p;
  3 }

In clasa de test TestTagsCounter adaugati metoda de test de mai jos (observati cum creeaza o clasa interna anonima, ce extinde clasa HttpStringProvider)

   1 @Test
   2 public void testCounterWithAnonimous() {
   3         TagsParser counter = new TagsParser("http://www.loose.upt.ro/vvswiki") ;
   4         counter.setProvider(new HttpStringProvider(){
   5                 @Override
   6                 public String getStringForAddress(String urlAddress) {
   7                         return "";
   8                 }
   9         });
  10         assertEquals(0, counter.getTagsCount());
  11 }

Putem face acelasi lucru cu o clasa interna cu nume, in clasa de test TestTagsCounter, ca mai jos:

   1 private static class HttpStringProviderOneTag extends HttpStringProvider {
   2         @Override
   3         public String getStringForAddress(String urlAddress) {
   4                 return "<html></html>";
   5         }
   6 }

O alta varianta, ce ne ofera mai multa flexibilitate, ar fi sa construim o singura clasa mock generica, pe care sa o putem refolosi pentru toate testele pe care le vom scrie:

   1 private static class GenericHttpProviderMock extends HttpStringProvider {
   2         @Override
   3         public String getStringForAddress(String u) {
   4                 return u;
   5         }
   6 }

Pentru clasa mock generica puteti vedea un model de utilizare in metoda de test de mai jos:

   1 @Test
   2 public void testCounterWithAnonimous() {
   3         TagsParser counter = new TagsParser("<b><b></b>") ;
   4         counter.setProvider(new GenericHttpProviderMock());
   5         assertEquals(2, counter.getTagsCount());
   6 }

Pasul 3 - Mockito

Codul de test de mai sus introduce un tip de testare care foloseste obiecte mock, sau pseudo-obiecte, in locul obiectelor reale cu care ar trebui sa interactioneze codul nostru. Objectele mock sunt create cu scopul de a forta si de a putea verifica anumite scenarii de interactiune pe care codul nostru trebuie sa le realizeze. Astfel, testarea cu mock-uri ne ajuta sa facem cu adevarat testare unitara, sa ne testam propriul cod in izolare, evitand complexitatea pe care o implica interactiunile reale cu codul diverselor biblioteci cu care lucram. Aceasta situatie e frecventa intr-o gama larga de proiecte, astfel ca exista multe biblioteci care ne pot ajuta sa cream si sa folosim obiecte mock / pseudo-obiecte pentru a scrie teste unitare (si nu numai). Una dintre cele mai folosite astfel de biblioteci este EasyMock, impreuna cu derivata sa foarte populara Mockito.

In cele ce urmeaza vom evidentia cateva functionalitati de baza oferite de Mockito, dupa care vom relua exemplul de la Pasul 2 folosind Mockito.

Descarcam intai ultima versiune de Mockito, dupa care vom crea un nou proiect Eclipse, cu numele de MockitoTest. Dupa crearea proiectului, adaugam biblioteca externa din arhiva descarcata astfel: selectam proiectul > right click pe proiect > selectam Build Path > Add External Archives >> alegem biblioteca Mockito (mockito-all-X.X.X.jar, unde X.X.X reprezinta versiunea bibliotecii).

Apoi cream o clasa de test noua cu numele PlaygroundTest, ca mai jos:

   1 import static org.mockito.Mockito.*;
   2 import java.util.List;
   3 import org.junit.Test;
   4 
   5 public class PlaygroundTest {
   6 
   7         @Test
   8         public void invocation() {
   9                 //mock creation
  10                 List<String> mockedList = mock(List.class);
  11 
  12                 //using mock object
  13                 mockedList.add("one");
  14                 mockedList.clear();
  15 
  16                 //verification
  17                 verify(mockedList).add("one");
  18                 verify(mockedList).clear();
  19         }
  20 }

In prima metoda de test vom crea un obiect mock pentru tipul java.util.List (de regula, conform bunelor practici, nu vom crea obiecte mock pentru tipuri container / colectii, ci vom folosi colectiile propriu-zise, dar populandu-le cu obiecte mock; dar in acest caz am ales tipul List pentru simplitatea sa, si pentru ca reprezinta o interfata standard foarte bine cunoscuta tuturor). Observati ca pentru a construi obiectul mock pentru tipul List tot ce a trebuit sa facem a fost sa invocam metoda statica mock() oferita de Mockito. Pe obiectul astfel creat putem apela metodele tipice interfetei List, cum ar fi add sau clear, si putem apoi verifica daca ele au fost apelate folosind metoda statica verify a Mockito. Observati ca pentru metoda add verificam si daca a fost apelata cu un parametru anume, in cazul de fata cu sirul de caracatere "one".

Deci Mockito ne ofera metoda statica verify, cu ajutorul careia putem sa ne asiguram ca anumite metode au fost invocate pe pseudo-obiectele create de noi si cu care codul nostru interactioneaza. Putem verifica si de cate ori au fost invocate (default e 1), si cu ce parametri anume.

Vom scrie acum o metoda de test de mai jos:

   1 @Test(expected = RuntimeException.class)
   2 public void stubbing() {
   3         // You can mock concrete classes, not only interfaces
   4         @SuppressWarnings("unchecked")
   5         LinkedList<String> mockedList = mock(LinkedList.class);
   6 
   7         // stubbing
   8         when(mockedList.get(0)).thenReturn("first");
   9         when(mockedList.get(1)).thenThrow(new RuntimeException());
  10 
  11         Assert.assertEquals("first", mockedList.get(0));
  12 
  13         // "null" because get(999) was not stubbed
  14         Assert.assertEquals(null, mockedList.get(999));
  15 
  16         // following throws runtime exception
  17         System.out.println(mockedList.get(1));
  18 
  19         // Although it is possible to verify a stubbed invocation, usually it's
  20         // just redundant
  21         // If your code cares what get(0) returns then something else breaks
  22         // (often before even verify() gets executed).
  23         // If your code doesn't care what get(0) returns then it should not be
  24         // stubbed.
  25         verify(mockedList).get(0);
  26 }

Aici mergem mai departe si preprogramam obiectul nostru mock sa raspunda intr-un anumit mod, dupa necesitati, atunci cand apelam o anumita metoda pe el. Facem asta folosindu-ne de metoda statica when oferita de Mockito. Cu ajutorul ei am preprogramat pseudo-obiectul nostru in asa fel incat daca metoda get este apelata cu parametrul 0 va returna sirul de caractere "first", iar daca e apelata cu parametrul 1 va arunca o exceptie de tipul RuntimeException.

   1 @Test
   2 public void matchers() {
   3         @SuppressWarnings("unchecked")
   4         LinkedList<String> mockedList = mock(LinkedList.class);
   5         // stubbing using built-in anyInt() argument matcher
   6         when(mockedList.get(anyInt())).thenReturn("element");
   7 
   8         Assert.assertEquals("element", mockedList.get(0));
   9         Assert.assertEquals("element", mockedList.get(999));
  10 
  11         // you can also verify using an argument matcher
  12         verify(mockedList, times(2)).get(anyInt());
  13 }

In metoda de test de aici putem observa un alt mod de a preprograma obiectul mock: apelul la metoda get() va returna rezultatul preprogramat indiferent de valoarea concreta a parametrului sau efectiv, atata timp cat acesta este de tipul primitiv int. Acest lucru se realizeaza prin intermediul unei metode de tip matcher, oferita de Mockito, una dintre multele astfel de metode disponibile in biblioteca. Acestea pot fi folosite nu doar pentru a preprograma mock-uri, dar si pentru a verifica (la un mod mai general) apelurile de metoda efectuate pe mock. Nu vom insista mai mult pe diversele si foarte utilele matchers disponibile, dar va invitam sa consultati documentatia oficiala pentru a va familiariza cu ele.

   1 @Test
   2 @SuppressWarnings("unchecked")
   3 public void invocationTimes() {         
   4         LinkedList<String> mockedList = mock(LinkedList.class);
   5         mockedList.add("once");
   6         mockedList.add("once again");
   7 
   8         mockedList.add("twice");
   9         mockedList.add("twice");
  10 
  11         mockedList.add("three times");
  12         mockedList.add("three times");
  13         mockedList.add("three times");
  14 
  15         // following two verifications work exactly the same - times(1) is used
  16         // by default
  17         verify(mockedList).add("once");
  18         verify(mockedList, times(1)).add("once");
  19 
  20         // exact number of invocations verification
  21         verify(mockedList, times(2)).add("twice");
  22         verify(mockedList, times(3)).add("three times");
  23 
  24         // verification using never(). never() is an alias to times(0)
  25         verify(mockedList, never()).add("never happened");
  26 
  27         // verification using atLeast()/atMost()
  28         verify(mockedList, atLeastOnce()).add("three times");
  29         verify(mockedList, atLeast(2)).add("three times");
  30         verify(mockedList, atMost(5)).add("three times");
  31 
  32         // create an inOrder verifier for a single mock
  33         InOrder inOrder = inOrder(mockedList);
  34 
  35         // following will make sure that add is first called with
  36         // "was added first, then with "was added second"
  37         inOrder.verify(mockedList).add("once");
  38         inOrder.verify(mockedList).add("once again");
  39 
  40         // verify that method was never called on a mock
  41         verify(mockedList, never()).add("two");
  42 
  43         List<String> firstMock = mock(List.class);
  44         List<String> secondMock = mock(List.class);
  45 
  46         // verify that other mocks were not interacted
  47         verifyZeroInteractions(firstMock, secondMock);
  48 }

In metoda de test de mai sus am realizat o serie de verificari, pentru a explora posibilitatile disponibile: ca metodele mock-ului au fost apelate de un anumit numar de ori, ca nu au fost apelate de loc sau ca programul nostru nu a interactionat in niciun fel cu obiectul mock pe parcursul testului. Toate acestea sunt cazuri de utilizare frecvente ale bibliotecii Mockito, care ne poate astfel simplifica puternic modul de scriere al testelor unitare si nu numai. Pentru mai multe informatii va rugam sa consultati documentatia oficiala a Mockito.

Acum ne putem intoarce la exemplul de la Pasul 2, si putem incerca sa il rescriem folosind de data asta Mockito pentru mock testing.

   1 @Test
   2 public void testCounterWithAnonimous() {
   3         HttpStringProvider mockStringProvide = mock(HttpStringProvider.class);
   4         when(mockStringProvide.getStringForAdress(anyString())).thenReturn("<b><b></b></b>");
   5         
   6         TagsParser counter = new TagsParser("http_address");
   7         counter.setProvider(mockStringProvide);
   8         
   9         assertEquals(2, counter.getTagsCount());
  10         verify(mockStringProvide).getStringForAdress("http_address");
  11 }

Observati ca am creat un obiect mock de tipul HttpStringProvider, si ca l-am preprogramat sa returneze un string continand 2 tag-uri ("<b><b></b></b>") indiferent de valoarea concreta a sirului de caractere cu care va fi apelata metoda getStringForAdress. Apoi, penttru initializarea obiectului de tip TagsParser pe care il cream in test am folosit o constanta sir de caractere ("http_address"), si apoi am verificat ca acelasi sir a fost pasat mai departe ca parametru la apelul metodei getStringForAdress. Observati deci ca Mockito ne ofera nu doar mai multe posibilitati de testare a interactiunilor dintre codul nostru si terte parti (diverse biblioteci etc.), ci ne si ajuta sa scapam de o parte din codul pe care l-am scrie in mod normal in cadrul unei abordari naive de testare cu ajutorul mock-urilor, cum e cazul la Pasul 2. Acest lucru are ca efect simplificarea testelor rezultate, care devin astfel semnificativ mai usor de citit si de inteles.