JUnit - instrukcja

  1. Ściągnąć z sieci, spod adresu Wumpus.zip projekt “Hunt the Wumpus”.

  2. Projekt ten to “kultowa” gra znana właśnie pod tytułem “Hunt the Wumpus”. Jeżeli gra ta mimo swojej “kultowości” jest jednak komuś nieznana, to informacje na jej temat można znaleźć tu.

  3. Rozpakować ten projekt i otworzyć w NetBeans.
  4. Odnaleźć w projekcie klasę wumpus.CommandParser i  sporządzić dla niej testy jednostkowe.
    1. Gra "Hunt hte Wumpus" ma tekstowy interfejs. Gracz wydaje polecenia wpisując je na standardowym wejściu. W związku z tym potrzebna jest klasa, która polecenia wydawane przez gracza będzie parsować. Klasą tą jest wumpus.CommandParser.
      3.1. Wykorzystując środowisko NetBeans sporządzić szkielet testów dla klasy wumpus.CommandParser.
W menu dostępnym pod prawym przyciskiem myszy należy wybrać "Create JUnit Tests"
Tworząc szablon testów jednostkowy można ustalić:
  •  dla których metod mają być wygenerowane metody testujące (Method Access Level), 
  • czy wygenerować metody SetUp, TearDown (o ich zastosowaniu będzie za chwilę) oraz domyślne ciała metod testujących (Deafult Method Bodies - opcja ta jest niezalecana z uwagi na niską jakość generowanych testów)
  • czy automatycznie wygenerować komentarze.
W przerabianym tu przykładzie najlepiej wybrać opcja tak jak na ilustracji obok.
      3.2. Do klasy wumpus.CommandParserTest dopisać metodę testGetCommand (assertTrue).
          public void testGetCommand()
          {
              CommandParser commandParser = new CommandParser();
      /*W obiekcie CommandParser trzeba zarejestrować obsługiwane polecenia nim zacznie się je parsować. Robi się to przy pomocy metody add(). Pierwszy parametr tej metody to String z nazwą polecenia, natomiast drugi to obiekt reprezentujący polecenie. */
              commandParser.add("a", MoveEastCommand.class);
      /*Sprawdzamy działanie metody getCommand. Metoda ta ma za zadanie zwrócić obiekt reprezentujący polecenie, którego nazwą jest parametr, z którym metoda getCommand jest wywoływana. Funkcja assertTrue to właściwy test. Wszystkie metody z assert w nazwie służa do sprawdzania warunków i jeżeli sprawdzany warunek nie jest spełniony to rzucany jest wyjątek i test zawodzi. assertTrue sprawdza warunki logiczne i rzuca wyjątek, gdy warunek jest równy false (dostępna jest również metoda assertFalse).*/
              assertTrue(commandParser.getCommand("a") instanceof MoveEastCommand);
          }

      3.3. Do klasy wumpus.CommandParserTest dopisać metodę test testNoCommand (assertNull).

          public void testNoCommand()
          {
              CommandParser commandParser = new CommandParser();
      /*Sprawdzamy, czy metoda getCommand zwraca null, jeżeli w obiekcie CommandParser nie zarejestrowano żadnych poleceń. Do sprawdzania używamy metody assertNull. Metoda ta rzuca wyjątek, gdy jej parametr jest różny od null (dostępna jest również metoda assertNotNull).*/
              assertNull(commandParser.getCommand("a"));
          }
3.4. Do klasy wumpus.CommandParserTest dopisać metodę test testStringTrimmer (assertEquals).
    public void testStringTrimmer()
    {
        CommandParser parser = new CommandParser();
/*Sprawdzamy, czy metoda removeLastWord usówa ostatnie słowo z będącego parametrem Stringa. Metoda assertEquals porównuje będące parametrami obiekty i rzuca wyjątek, jeżeli obiekty te nie są sobie równe (dostępna jest również metoda assertNotEquals).*/
        assertEquals("", parser.removeLastWord("a"));
        assertEquals("a b c d", parser.removeLastWord("a b c d e"));
        assertEquals("a b c  d", parser.removeLastWord("a b c  d   e"));
        assertEquals("", parser.removeLastWord(""));
        assertNull(parser.removeLastWord(null));
    }
Proszę zastanowić się dlaczego w metodzie testStringTrimmer() jest aż pięc testów. Czy jest to wystarczająca ilość do dokładnego przetestowania metody removeLastWord()?
3.4. Do klasy wumpus.CommandParserTest dopisać metodę testMemoryManagement (assertSame).
   
public void testMemoryManagement()
    {
        CommandParser parser = new CommandParser();
        parser.add("a", MoveEastCommand.class);
        Command first = parser.getCommand("a");
        Command second = parser.getCommand("a");
/*Sprawdzamy, czy metoda getCommand oszczędnie gospodaruje pamięcią. Czyli, czy metoda getCommand, zwracając to samo polecenie, zwraca ten sam obiekt, czy też za każdym razem tworzy nowy obiekt. Ponieważ test ten dotyczy kwestii wydajnościowych, które są drugorzędne, test opatrzono komentarzem "This test doesn't have to pass". Komentarze, takie jak ten, można stosować również w pozostałych metodach typu assert. Komentarze w assertach usprawniają pracę z testami ułatwiając identyfikowanie testu, który zawiódł, a czasami również powodu dla którego test zawiódł. Metoda assertSame sprawdza, czy będące parametrami referencje wskazują na ten sam obiekt. Jeżeli wskazują na różne obiekty, to zostanie rzucony wyjątek i test zawiedzie.*/
        assertSame("This test doesn't have to pass.", first, second);
    }
  1. Przetestować klasę wumpus.CommandParser.
  2. 4.1 Uruchomić testy jednostkowe.
    Testy jednostkowe uruchamia się wybierając Test Project z menu dostępnego pod prawym przyciskiem myszy (i na kilka innych sposobów).
    4.2 Poprawić błędy, jeżeli jakieś zostały wykryte podczas testowania.
    Metodę testującą testMemoryManagement() można wykomentować. Natomiast pozostałe problemy należy rozwiązać.
  3. Sporządzić testy jednostkowe dla klasy wumpus.Direction.
    1. public class DirectionTest extends TestCase
      { 
          public DirectionTest(String testName)
          {
              super(testName);
          }
/*Metoda setUp jest metodą opcjonalną - można jej w klasie testowej nie implementować. Ma ona za zadanie przygotować środowisko testowe. Jest ona automatycznie wywoływana przed każą metodą testową. W prezentowanym tu przykładzie metoda setUp spowoduje, że w każdej metodzie testowej będzie stosowany ten sam generator liczb pseudolosowych (stosowany tu generator losuje zawsze tą samą sekwencję liczb, czyli: 0 1 2 3 4; z losowością nie ma więc nic wspólnego). Istnieje symetryczna do setUp metoda o nazwie tearDown - tearDown jest wywoływane automatycznie po każdej metodzie testowej. Służy do sprzątania po testach.*/
          protected void setUp() throws Exception
          {
              int seq[] = new int[]{0,1,2,3,4};
              Direction.setRandomNumberGenerator(new PseudoRandomNumberGenerator(seq));
          }

      /*
       Test of getRandomDirection method, of class wumpus.Direction.
      */
          public void testGetRandomDirection()
          {
              assertEquals(Direction.NORTH,Direction.getRandomDirection());
              assertEquals(Direction.SOUTH,Direction.getRandomDirection());
              assertEquals(Direction.EAST,Direction.getRandomDirection());
              assertEquals(Direction.WEST,Direction.getRandomDirection());
/*Znajdująca się poniżej sekcja try-catch to standardowy test testujący rzucanie wyjątku. Test ten zostanie zaliczony wtedy, kiedy Direction.getRandomDirection() rzuci wyjątek. Natomiast wtedy, kiedy wyjątek nie zostanie z Direction.getRandomDirection() rzucony, to test nie przejdzie. Stanie się tak dlatego, że wywołana zostanie wtedy metoda fail(), która bezwarunkowo powoduje niezaliczenie testu.*/
              try
              {
                  Direction.getRandomDirection();
                  fail("no excpetion thrown");
              }
              catch(IllegalArgumentException e)
              {
                  // expected
              }
          }
         
      /**

       * Test of setRandomNumberGenerator method, of class wumpus.Direction.
       */
          public void testSetRandomNumberGenerator()
          {
          }

      /**
       * Test of makePleasantDirectionName method, of class wumpus.Direction.
       */
          public void testMakePleasantDirectionName()
          {
          }

      /**
       * Test of getOpposite method, of class wumpus.Direction.
       */
          public void testGetOpposite()
          {
          }   
      }
      Metody o pustych ciałach należy uzupełnić, tak aby testowały odpowiadające im metody z klasy wumpus.Direction (nazwy tych metod znajdują się w komentarzach).
  1. Poprawić błędy wykryte przy pomocy testów z poprzedniego punktu.
  2. Opracować przypadek testowy metody executeCommand() z klasy WumpusMain dla komendy "e".
Najistotniejszym elementem metody executeCommand() jest interakcja z instancjami klas WumpusGame oraz CommandParser. Do przetestowania przebiegu interakcji można użyć klas typu mock:
 
package wumpus;

import junit.framework.TestCase;
import static org.easymock.EasyMock.*;

public class WumpusMainTest extends TestCase {
   
    public WumpusMainTest(String testName) {
        super(testName);
    }


    /**
     * Test of executeCommand method, of class WumpusMain.
     */

    public void testExecuteCommand() {
        String command = "e";
        CommandParser parserMock = createMock(CommandParser.class);
       
        expect( parserMock.getCommand(command) )   //mock w trybie konfigurowania
                .andReturn( new MoveEastCommand() )
                .times(1);
       
        replay(parserMock);     //mock przechodzi w tryb odtwarzania
       
        WumpusMain main = new WumpusMain( ... );
        main.setCommandParser(parserMock);
       
        main.executeCommand(command);
       
        verify(parserMock);    //weryfikujemy czy mialy m,iejsce zadeklarowane wywoalnia metod
    }
}

Przedstawiony przykład pokazuje jak użyć klasy typu mock do zastąpienia instancji klasy CommandParser w teście sprawdzającym interakcje po wykonaniu komendy "e" (przejście do wschodniej komnaty). Aby test ten był wartościowy należy jeszcze w podobny sposób zastąpić instancję klasy WumpusGame.
  1. Opracować przypadki testowe dla przynajmniej 2 innych, obsługiwanych komend (np. "move e" i "shoot magic").
  2. Wyeliminować z opracowanego kodu powtórzenia.
  3. Opracować test jednostkowy dla metody configureCommandParser() z klasy WumpusMain.
W testowanej metodzie występuje wiele, bardzo podobnych wywołań metod. Proszę zastanowić się nad możliwościami zminimalizowania liczby linii kodu w teście. Pomocne mogą być oferowane przez EasyMock funkcjonalności związane z dopasowywaniem do zadanego wyrażenia regularnego oraz metoda ustalająca krotność wywołania (times()).
  1. Wyeliminować powstałe powtórzenia kodu.
Warto rozważyć zastosowanie metody setUp().