Unit и Coverage тестирование Java проектов

Описание Java проекта

Приложение EnglishTesting создано для тестирования английского языка. Оно выводит вопрос - Английское слово и запрашивает от пользователя перевод. Слова хранятся в БД(mysql). Для того чтобы приложение заработало Вам необходимо :

  1. Создать БД : CREATE DATABASE englishwords CHARACTER SET utf8;
  2. Создать в БД нужную структуру(файл есть в архиве): # cat englishwords.sql | mysql -uuser -ppasswd englishwords
  3. Заполнить БД тестовыми данными: # cat dump_words.sql|mysql -uuser -ppasswd englishwords
Схема приложения показана ниже:

Также Вам необходимо скачать JDBC driver для MYSQL и прописать его в конфигурационном файле:
CLASSPATH=./:/home/JavaClasses:/home/JavaClasses/EnglishTesting.jar:/home/JavaClasses/mysql-connector-java-5.1.14-bin.jar
После всех корректных настроек(также проверьте правильность подключения драйвера в проекте) использовать приложение можно следующим образом:
# java userconsole.ViewTesting
Вопрос № 1
howling
Перевод :
....
Дата: Sat Feb 05 19:19:08 MSK 2011
Результат 50%
Правильных ответов 2 из 4
Ошибок 2
Неправильный ответ на вопрос № 2
Слово perform
Ваш ответ .
Правильный ответ [выполнять, исполнять, делать]
...
Исходный код проекта EnglishTesting.tar.gz.

Unit-тесты с использованием заглушек

Теперь покроем юнит-тестами созданный класс EnglishTesting.java. Для того, чтобы протестировать методы( классы), которые в свою очередь зависят от других классов необходимо использовать заглушки (stub).
Так как класс EnglishTesting зависит от класса DataConnection, а нам нужно протестировать именно работоспособность EnglishTesting, то мы с помощью интерфейса DataConnection в тестовом классе EnglishTestingTest создадим заглушку на DataConnection. Таким образом, всегда можно протестировать классы независимо друг от друга - в этом и есть суть юнит-тестирования.
Начнем с метода, который использует класс DataConnection в качестве переменной. Это метод getEngWord():

public class EnglishTesting {
...
  public String getEngWord(DataConnection data){
    String englishWord;
    englishWord = data.returnRandomWord();
    return englishWord;
  }
...
}

Этот метод просто возвращает слово, полученное от объекта data методом returnRandomWord().
Теперь создадим заглушку StubDBconnect :

public class StubDBconnect implements DataConnection{
  public String returnRandomWord() {
    return "Hello";
  }
  public ArrayList returnAnswersByWord(String englishWord) {
    ArrayList answersList = new ArrayList();
    answersList.add("привет");
    return answersList;
  }
} 

Теперь наш Test Case c одним тестовым методом будет выглядеть следующим образом:

package testing;
import connection.DataConnection;
import java.util.ArrayList;
import org.junit.Test;
import static org.junit.Assert.*;
public class EnglishTestingTest {
  /**
   * Stub class - Data Connection
   */
  public class StubDBconnect implements DataConnection{
    public String returnRandomWord() {
      return "Hello";
    }
    public ArrayList returnAnswersByWord(String englishWord) {
      ArrayList answersList = new ArrayList();
      answersList.add("привет");
      return answersList;
    }
  }
  @Test
  public void testGetEnglishWord(){       
    StubDBconnect data = new StubDBconnect();
    EnglishTesting test = new EnglishTesting();
    String word = test.getEngWord(data);
    assertEquals("Hello",word);        
  } 
}

Таким образом в assertEquals() мы сравниваем эталон - "Hello" - то тестовое слово , которое всегда возвращает заглушка с тем, что получили методом getEngWord(DataConnection data).
В предыдущей статье уже говорилось о возможных вариантах проверок.
Аналогично рассмотрим следующий метод checkAnswer() класса EnglishTesting, который необходимо проотестировать:

public class EnglishTesting {
...
  boolean checkAnswer(String englishWord, String russianWord,DataConnection data){       
    ArrayList answersList = data.returnAnswersByWord(englishWord);
    return answersList.contains(russianWord);
  }
...
}

Данный метод также использует public метод returnAnswersByWord() объекта класса DataConnection. Поэтому для создания unit-теста мы опять будем использовать заглушку:

public class EnglishTestingTest {
  @Test
  public void testCheckAnswer(){        
    StubDBconnect data = new StubDBconnect();
    EnglishTesting test = new EnglishTesting();
    assertTrue(test.checkAnswer("Hello", "привет", data));
  }
...
}

Дадим всем методам класса EnglishTesting пакетный доступ для того, чтобы их можно было покрыть unit-тестами. Для тех, которые используются в Main классе дадим доступ public.
Теперь рассмотрим внутренний метод calcPercent() он использует 2 переменные объекта countQuests и skill и возвращает процент правильных ответов пользователя в качестве строки, например, "50%"

public class EnglishTesting {
...
  String calcPercent(){
    if (countQuests>0){
      double percent = skill.doubleValue()/countQuests.doubleValue();           
      NumberFormat percentFormat = NumberFormat.getPercentInstance();
      percentFormat.setMaximumFractionDigits(1);
      String result = percentFormat.format(percent);
      return result;
    }
    else
    {
      return "100%";
    }        
  }
...
}

Для создания теста этого метода заглушка, нам уже не нужна , но необходимо задать тестовые значения переменных объекта. А именно:

public class EnglishTestingTest {
  ...
  @Test
  public void testCalcPercent(){
    EnglishTesting test = new EnglishTesting();
    //Test data
    test.countQuests=4;
    test.skill = 2;
    assertEquals("50%", test.calcPercent());
  }
...
}

После вынесения объектов data и test в метод @Before наш Test Case примет следующий вид :

package testing;
import org.junit.Before;
import connection.DataConnection;
import java.util.ArrayList;
import org.junit.Test;
import static org.junit.Assert.*;

/**
 * JUnit tests for EnglishTesting.java
 * Testing logic which works with DB
 * Testing logic methods getResults(), calcPercent(),calcSkill()
 * @author Shpatserman Maria
 */
public class EnglishTestingTest {
  /**
   * Stub class - Data Connection
   */
  public class StubDBconnect implements DataConnection{
    public String returnRandomWord() {
      return "Hello";
    }
    public ArrayList returnAnswersByWord(String englishWord) {
      ArrayList answersList = new ArrayList();
      answersList.add("привет");
      return answersList;
    }
  }
  private StubDBconnect data;
  private EnglishTesting test;
  @Before
  public void instantiate() {
    test = new EnglishTesting();
    data = new StubDBconnect();       
  }
  //Logic tests
  @Test
  public void testCalcPercentNullQuestions(){
    test.countQuests=0;
    assertEquals("100%",test.calcPercent());
  }
  @Test
  public void testCalcPercent(){
    //Test data
    test.countQuests=4;
    test.skill = 2;
    assertEquals("50%", test.calcPercent());
  }
  @Test
  public void testCalcSkillIncrement(){
    //Test data
    test.countQuests=0;
    test.skill = 0;
    //CorrectAnswer
    test.calcSkill("Hello", "привет", 1, data);
    //Check skills
    assertTrue(test.skill==1);        
  }
  @Test
  public void testCalcSkillIncrementErrors(){
    //Test data
    test.countQuests=0;
    test.errors = 0;
    //IncorrectAnswer
    test.calcSkill("Hello", "пока", 1, data);
    //Check errors
    assertTrue(test.errors==1);
  }
  @Test
  public void testGetResultsExcelent(){
    //Test data
    test.errors = 0;
    test.countQuests =1;
    test.skill=1;
    String result = test.getResults();
    String expect = "Великолепно! \n"+
                    "Результат 100%\n"+
                    "Правильных ответов 1 из 1";
    assertEquals(expect, result);
  }
  @Test
  public void testGetResultsGood(){
    //Test data
    test.errors = 1;
    test.countQuests =2;
    test.skill=1;
    test.improperAnswers="";        
    String result = test.getResults();
    String expect = "Результат 50%\n"+
                    "Правильных ответов 1 из 2\n"+
                    "Ошибок 1\n";
    assertEquals(expect, result);
  }
  //Work with Data
  @Test
  public void testGetEnglishWord(){        
    //StubDBconnect data = new StubDBconnect();
    String word = test.getEngWord(data);
    assertEquals("Hello",word);        
  }
  @Test
  public void testCheckAnswer(){        
    //StubDBconnect data = new StubDBconnect();
    assertTrue(test.checkAnswer("Hello", "привет", data));
  }
  @Test
  public void testCheckIncorrectAnswer(){        
    //StubDBconnect data = new StubDBconnect();
    assertFalse(test.checkAnswer("Hello", "пока", data));
  }
  @Test
  public void testGenerateAnswerForImproperWord(){        
    //StubDBconnect data = new StubDBconnect();
    String result = test.generateAnswerForImproperWord("Hello", "пока", 1, data);
    String standard = "Неправильный ответ на вопрос № 1\n"+
                      "Слово Hello\n"+
                      "Ваш ответ пока\n"+
                      "Правильный ответ [привет]";
    assertEquals(standard,result);
  }   
}

При проверке кода EnglishTestingTest.txt в IDE Netbeans 6.5 Вы получите следующий результат:

Измерение покрытия кода тестами

Для того, чтобы проверить покрытие Unit-тестами исходного кода необходимо использовать какой-нибудь plugin. К примеру, в Netbeans можно установить plugin EMMA (Code Coverage Plugin). Для этого нужно проделать следующие действия:

  1. Выбрать в меню Tools-Plugins. Далее перейти на вкладку Available Plugins и выбрать Code Coverage Plugin, как на рисунке

  2. После этого при нажатии Right Click по проекту в дереве появится возможность выбрать пункт Coverage -> Activate Coverage Collection.
  3. Теперь при нажатии Right Click выбираем пункт Test. Таким образом все наши тесты проходят.
  4. Далее просматриваем результаты прогона тестов, при этом нужно выбрать Coverage -> Show Project Coverage Statistics

    Здесь можно увидеть сколько строк кода покрыто и сколько еще нужно покрыть.

Использование Test Suite

Если есть необходимость запускать сразу несколько файлов с тестами одновременно , то можно воспользоваться Test Suite эту возможность также предоставляет JUnit 4.
Для этого Вам нужно создать новый класс, к примеру, SuiteTests, как показано ниже:

//:suite/SuiteTests.java
package suite;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
/**
 * JUnit  - suite class
 * @author Shpatserman Maria
 */
@RunWith(value=Suite.class)
@SuiteClasses(value = {
              testing.EnglishTestingTest.class
              })
public class SuiteTests {
}

Suite - это контейнер из нескольких тестов. Для того, чтобы обозначить класс как Suite нужно использовать обозначение @RunWith(value=Suite.class), а также в блоке @SuiteClasses указать те тестовые классы, которые должны запускаться.

PS

В исходниках я также выложила perl скрипт insert.pl для распарсивания words.csv с добавлением новых слов в БД.