Как написать тесты на java

Время на прочтение
8 мин

Количество просмотров 517K


Сегодня все большую популярность приобретает test-driven development(TDD), техника разработки ПО, при которой сначала пишется тест на определенный функционал, а затем пишется реализация этого функционала. На практике все, конечно же, не настолько идеально, но в результате код не только написан и протестирован, но тесты как бы неявно задают требования к функционалу, а также показывают пример использования этого функционала.

Итак, техника довольно понятна, но встает вопрос, что использовать для написания этих самых тестов? В этой и других статьях я хотел бы поделиться своим опытом в использовании различных инструментов и техник для тестирования кода в Java.

Ну и начну с, пожалуй, самого известного, а потому и самого используемого фреймворка для тестирования — JUnit. Используется он в двух вариантах JUnit 3 и JUnit 4. Рассмотрю обе версии, так как в старых проектах до сих пор используется 3-я, которая поддерживает Java 1.4.

Я не претендую на автора каких-либо оригинальных идей, и возможно многим все, о чем будет рассказано в статье, знакомо. Но если вам все еще интересно, то добро пожаловать под кат.

JUnit 3

Для создания теста нужно унаследовать тест-класс от TestCase, переопределить методы setUp и tearDown если надо, ну и самое главное — создать тестовые методы(должны начинаться с test). При запуске теста сначала создается экземляр тест-класса(для каждого теста в классе отдельный экземпляр класса), затем выполняется метод setUp, запускается сам тест, ну и в завершение выполняется метод tearDown. Если какой-либо из методов выбрасывает исключение, тест считается провалившимся.

Примечание: тестовые методы должны быть public void, могут быть static.

Сами тесты состоят из выполнения некоторого кода и проверок. Проверки чаще всего выполняются с помощью класса Assert хотя иногда используют ключевое слово assert.

Рассмотрим пример. Есть утилита для работы со строками, есть методы для проверки пустой строки и представления последовательности байт в виде 16-ричной строки:

public abstract class StringUtils {
  private static final int HI_BYTE_MASK = 0xf0;
  private static final int LOW_BYTE_MASK = 0x0f;

  private static final char[] HEX_SYMBOLS = {
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
  };

  public static boolean isEmpty(final CharSequence sequence) {
    return sequence == null || sequence.length() <= 0;
  }

  public static String toHexString(final byte[] data) {
    final StringBuffer builder = new StringBuffer(2 * data.length);
    for (byte item : data) {
      builder.append(HEX_SYMBOLS[(HI_BYTE_MASK & item) >>> 4]);
      builder.append(HEX_SYMBOLS[(LOW_BYTE_MASK & item)]);
    }
    return builder.toString();
  }
}

Напишем для нее тесты, используя JUnit 3. Удобнее всего, на мой взгляд, писать тесты, рассматривая нейкий класс как черный ящик, писать отдельный тест на каждый значимый метод в этом классе, для каждого набора входных параметров какой-то ожидаемый результат. Например, тест для isEmpty метода:

  public void testIsEmpty() {
    boolean actual = StringUtils.isEmpty(null);
    assertTrue(actual);

    actual = StringUtils.isEmpty("");
    assertTrue(actual);

    actual = StringUtils.isEmpty(" ");
    assertFalse(actual);

    actual = StringUtils.isEmpty("some string");
    assertFalse(actual);
  }

Можно разделить данные и логику теста, перенеся создание данных в метод setUp:

public class StringUtilsJUnit3Test extends TestCase {
  private final Map toHexStringData = new HashMap();

  protected void setUp() throws Exception {
    toHexStringData.put("", new byte[0]);
    toHexStringData.put("01020d112d7f", new byte[] { 1, 2, 13, 17, 45, 127 });
    toHexStringData.put("00fff21180", new byte[] { 0, -1, -14, 17, -128 });
    //...
  }

  protected void tearDown() throws Exception {
    toHexStringData.clear();
  }

  public void testToHexString() {
    for (Iterator iterator = toHexStringData.keySet().iterator(); iterator.hasNext();) {
      final String expected = (String) iterator.next();
      final byte[] testData = (byte[]) toHexStringData.get(expected);
      final String actual = StringUtils.toHexString(testData);
      assertEquals(expected, actual);
    }
  }

  //...
}

Дополнительные возможности

Кроме того, что было описано, есть еще несколько дополнительных возможностей. Например, можно группировать тесты. Для этого нужно использовать класс TestSuite:

public class StringUtilsJUnit3TestSuite extends TestSuite {
  public StringUtilsJUnit3TestSuite() {
    addTestSuite(StringUtilsJUnit3Test.class);
    addTestSuite(OtherTest1.class);
    addTestSuite(OtherTest2.class);
  }
}

Можно запустить один и тот же тест несколько раз. Для этого используем RepeatedTest:

public class StringUtilsJUnit3RepeatedTest extends RepeatedTest {
  public StringUtilsJUnit3RepeatedTest() {
    super(new StringUtilsJUnit3Test(), 100);
  }
}

Наследуя тест-класс от ExceptionTestCase, можно проверить что-либо на выброс исключения:

public class StringUtilsJUnit3ExceptionTest extends ExceptionTestCase {
  public StringUtilsJUnit3ExceptionTest(final String name) {
    super(name, NullPointerException.class);
  }

  public void testToHexString() {
    StringUtils.toHexString(null);
  }
}

Как видно из примеров все довольно просто, ничего лишнего, минимум нужный для тестирования(хотя недостает и некоторых нужных вещей).

JUnit 4

Здесь была добавлена поддержка новых возможностей из Java 5, тесты теперь могут быть объявлены с помощью аннотаций. При этом существует обратная совместимость с предыдущей версией фреймворка, практически все рассмотренные выше примеры будут работать и здесь(за исключением RepeatedTest, его нет в новой версии).

Итак, что же поменялось?

Основные аннотации

Рассмотрим тот же пример, но уже используя новые возможности:

public class StringUtilsJUnit4Test extends Assert {
  private final Map<String, byte[]> toHexStringData = new HashMap<String, byte[]>();

  @Before
  public static void setUpToHexStringData() {
    toHexStringData.put("", new byte[0]);
    toHexStringData.put("01020d112d7f", new byte[] { 1, 2, 13, 17, 45, 127 });
    toHexStringData.put("00fff21180", new byte[] { 0, -1, -14, 17, -128 });
    //...
  }

  @After
  public static void tearDownToHexStringData() {
    toHexStringData.clear();
  }

  @Test
  public void testToHexString() {
    for (Map.Entry<String, byte[]> entry : toHexStringData.entrySet()) {
      final byte[] testData = entry.getValue();
      final String expected = entry.getKey();
      final String actual = StringUtils.toHexString(testData);
      assertEquals(expected, actual);
    }
  }
}

Что мы здесь видим?

  • Для упрощения работы я предпочитаю наследоваться от класса Assert, хотя это необязательно.
  • Аннотация Before обозначает методы, которые будут вызваны до исполнения теста, методы должны быть public void. Здесь обычно размещаются предустановки для теста, в нашем случае это генерация тестовых данных (метод setUpToHexStringData).
  • Аннотация @BeforeClass обозначает методы, которые будут вызваны до создания экземпляра тест-класса, методы должны быть public static void. Имеет смысл размещать предустановки для теста в случае, когда класс содержит несколько тестов использующих различные предустановки, либо когда несколько тестов используют одни и те же данные, чтобы не тратить время на их создание для каждого теста.
  • Аннотация After обозначает методы, которые будут вызваны после выполнения теста, методы должны быть public void. Здесь размещаются операции освобождения ресурсов после теста, в нашем случае — очистка тестовых данных (метод tearDownToHexStringData).
  • Аннотация @AfterClass связана по смыслу с @BeforeClass, но выполняет методы после теста, как и в случае с @BeforeClass, методы должны быть public static void.
  • Аннотация Test обозначает тестовые методы. Как и ранее, эти методы должны быть public void. Здесь размещаются сами проверки. Кроме того, у данной аннотации есть два параметра, expected — задает ожидаемое исключение и timeout — задает время, по истечению которого тест считается провалившимся.

  @Test(expected = NullPointerException.class)
  public void testToHexStringWrong() {
    StringUtils.toHexString(null);
  }

  @Test(timeout = 1000)
  public void infinity() {
    while (true);
  }

Если какой-либо тест по какой-либо серьезной причине нужно отключить(например, этот тест постоянно валится, но его исправление отложено до светлого будущего) его можно зааннотировать @Ignore. Также, если поместить эту аннотацию на класс, то все тесты в этом классе будут отключены.

  @Ignore
  @Test(timeout = 1000)
  public void infinity() {
    while (true);
  }

Правила

Кроме всего вышеперечисленного есть довольно интересная вещь — правила. Правила это некое подобие утилит для тестов, которые добавляют функционал до и после выполнения теста.

Например, есть встроенные правила для задания таймаута для теста(Timeout), для задания ожидаемых исключений(ExpectedException), для работы с временными файлами(TemporaryFolder) и д.р. Для объявления правила необходимо создать public не static поле типа производного от MethodRule и зааннотировать его с помощью Rule.

public class OtherJUnit4Test {

  @Rule
  public final TemporaryFolder folder = new TemporaryFolder();

  @Rule
  public final Timeout timeout = new Timeout(1000);

  @Rule
  public final ExpectedException thrown = ExpectedException.none();

  @Ignore
  @Test
  public void anotherInfinity() {
    while (true);
  }

  @Test
  public void testFileWriting() throws IOException {
    final File log = folder.newFile("debug.log");
    final FileWriter logWriter = new FileWriter(log);
    logWriter.append("Hello, ");
    logWriter.append("World!!!");
    logWriter.flush();
    logWriter.close();
  }

  @Test
  public void testExpectedException() throws IOException {
    thrown.expect(NullPointerException.class);
    StringUtils.toHexString(null);
  }
}

Также в сети можно найти и другие варианты использования. Например, здесь рассмотрена возможность параллельного запуска теста.

Запускалки

Но и на этом возможности фреймворка не заканчиваются. То, как запускается тест, тоже может быть сконфигурировано с помощью @RunWith. При этом класс, указанный в аннотации должен наследоваться от Runner. Рассмотрим запускалки, идущие в комплекте с самим фреймворком.

JUnit4 — запускалка по умолчанию, как понятно из названия, предназначена для запуска JUnit 4 тестов.

JUnit38ClassRunner предназначен для запуска тестов, написанных с использованием JUnit 3.

SuiteMethod либо AllTests тоже предназначены для запуска JUnit 3 тестов. В отличие от предыдущей запускалки, в эту передается класс со статическим методом suite возвращающим тест(последовательность всех тестов).

Suite — эквивалент предыдущего, только для JUnit 4 тестов. Для настройки запускаемых тестов используется аннотация @SuiteClasses.

@Suite.SuiteClasses( { OtherJUnit4Test.class, StringUtilsJUnit4Test.class })
@RunWith(Suite.class)
public class JUnit4TestSuite {
}

Enclosed — то же, что и предыдущий вариант, но вместо настройки с помощью аннотации используются все внутренние классы.

Categories — попытка организовать тесты в категории(группы). Для этого тестам задается категория с помощью @Category, затем настраиваются запускаемые категории тестов в сюите. Это может выглядеть так:

public class StringUtilsJUnit4CategoriesTest extends Assert {
  //...

  @Category(Unit.class)
  @Test
  public void testIsEmpty() {
    //...
  }

  //...
}

@RunWith(Categories.class)
@Categories.IncludeCategory(Unit.class)
@Suite.SuiteClasses( { OtherJUnit4Test.class, StringUtilsJUnit4CategoriesTest.class })
public class JUnit4TestSuite {
}

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

@RunWith(Parameterized.class)
public class StringUtilsJUnit4ParameterizedTest extends Assert {
  private final CharSequence testData;
  private final boolean expected;

  public StringUtilsJUnit4ParameterizedTest(final CharSequence testData, final boolean expected) {
    this.testData = testData;
    this.expected = expected;
  }

  @Test
  public void testIsEmpty() {
    final boolean actual = StringUtils.isEmpty(testData);
    assertEquals(expected, actual);
  }

  @Parameterized.Parameters
  public static List<Object[]> isEmptyData() {
    return Arrays.asList(new Object[][] {
      { null, true },
      { "", true },
      { " ", false },
      { "some string", false },
    });
  }
}

Theories — чем-то схожа с предыдущей, но параметризирует тестовый метод, а не конструктор. Данные помечаются с помощью @DataPoints и @DataPoint, тестовый метод — с помощью Theory. Тест использующий этот функционал будет выглядеть примерно так:

@RunWith(Theories.class)
public class StringUtilsJUnit4TheoryTest extends Assert {

  @DataPoints
  public static Object[][] isEmptyData = new Object[][] {
      { "", true },
      { " ", false },
      { "some string", false },
  };

  @DataPoint
  public static Object[] nullData = new Object[] { null, true };

  @Theory
  public void testEmpty(final Object... testData) {
    final boolean actual = StringUtils.isEmpty((CharSequence) testData[0]);
    assertEquals(testData[1], actual);
  }
}

Как и в случае с правилами, в сети можно найти и другие варианты использования. Например, здесь рассмотрена та же возможность паралельного запуска теста, но с использованием запускалок.

Вывод

Это, конечно же, не все, что можно было сказать по JUnit-у, но я старался вкратце и по делу. Как видно, фреймворк достаточно прост в использовании, дополнительных возможностей немного, но есть возможность расширения с помощью правил и запускалок. Но несмотря на все это я все же предпочитаю TestNG с его мощным функционалом, о котором и расскажу в следующей статье.

Примеры можно найти на гитхабе.

Литература

  • JUnit
  • JUnit 4 in 60 Seconds
  • Using JUnit DataPoints and Theories
  • Using JUnit Parameterized Annotation
  • Writing your own JUnit extensions using @Rule
  • Scheduling Junit tests with RunnerScheduler for a concurrent execution

I provide this post for both IntelliJ and Eclipse.

Eclipse:

For making unit test for your project, please follow these steps (I am using Eclipse in order to write this test):

1- Click on New -> Java Project.

Create Project

2- Write down your project name and click on finish.

Create Project

3- Right click on your project. Then, click on New -> Class.

Create Class

4- Write down your class name and click on finish.

Create Class

Then, complete the class like this:

public class Math {
    int a, b;
    Math(int a, int b) {
        this.a = a;
        this.b = b;
    }
    public int add() {
        return a + b;
    }
}

5- Click on File -> New -> JUnit Test Case.

Create JUnite Test

6- Check setUp() and click on finish. SetUp() will be the place that you initialize your test.

Check SetUp()

7- Click on OK.

Add JUnit

8- Here, I simply add 7 and 10. So, I expect the answer to be 17. Complete your test class like this:

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class MathTest {
    Math math;
    @Before
    public void setUp() throws Exception {
        math = new Math(7, 10);
    }
    @Test
    public void testAdd() {
        Assert.assertEquals(17, math.add());
    }
}

9- Write click on your test class in package explorer and click on Run as -> JUnit Test.

Run JUnit Test

10- This is the result of the test.

Result of The Test

IntelliJ:
Note that I used IntelliJ IDEA community 2020.1 for the screenshots. Also, you need to set up your jre before these steps. I am using JDK 11.0.4.

1- Right-click on the main folder of your project-> new -> directory. You should call this ‘test’.
enter image description here
2- Right-click on the test folder and create the proper package. I suggest creating the same packaging names as the original class. Then, you right-click on the test directory -> mark directory as -> test sources root.
enter image description here
3- In the right package in the test directory, you need to create a Java class (I suggest to use Test.java).
enter image description here
4- In the created class, type ‘@Test’. Then, among the options that IntelliJ gives you, select Add ‘JUnitx’ to classpath.
enter image description here
enter image description here
5- Write your test method in your test class. The method signature is like:

@Test
public void test<name of original method>(){
...
}

You can do your assertions like below:

Assertions.assertTrue(f.flipEquiv(node1_1, node2_1));

These are the imports that I added:

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

enter image description here

This is the test that I wrote:
enter image description here

You can check your methods like below:

Assertions.assertEquals(<Expected>,<actual>);
Assertions.assertTrue(<actual>);
...

For running your unit tests, right-click on the test and click on Run .
enter image description here

If your test passes, the result will be like below:
enter image description here

В программировании для написания всевозможных программ и игр разработчик может использовать разнообразные функции и инструменты. Java является одним из самых простых и эффективных языков программирования, пользующихся спросом в 21 веке. У него есть средства, которые значительно упрощают процесс работы с создаваемыми утилитами. Одна из них носит название JUnit. Ей будет уделено основное внимание в данной статье.

Java – почему стоит использовать этот язык

Но перед тем, как изучать соответствующий элемент, программист должен определиться – действительно ли нужно работать с Джавой. Может, другой     язык программирования подойдет в большей степени для решения той или иной задачи.

Java не просто так пользуется спросом у разработчиков. Данный язык обладает немалым количеством сильных сторон. К ним относят:

  • функциональность;
  • наличие библиотек и фреймворков;
  • наличие собственной виртуальной машины, необходимой для успешной работы программного кода – контент способен работать «обособлено», без дополнительных расширений и драйверов;
  • скорость работы;
  • простой для понимания синтаксис, особенно если знать английский язык;
  • поддержку ООП;
  • возможность тестировать софт без существенных хлопот и лишних вмешательств со стороны.

Также Джава обладает высоким уровнем безопасности и наделен кроссплатформенностью. Переносить контент с одной операционной системы на другую удается без дополнительных корректировок.

Java активно применяется при создании:

  • сложных серьезных проектов;
  • веб-контента (но здесь чаще применяют JavaScript);
  • игр;
  • консольных утилит.

Универсальный вариант как для начинающих разработчиков, так и для тех, кто долгое время занимается программированием. Но после того, как программный код составлен, нужно провести его тест. Может быть, в процессе реализации соответствующей операции будут обнаружены какие-нибудь ошибки. Их придется исправлять, иначе софт не сможет нормально функционировать. Только не всегда ручной подход оказывается эффективным.

Фреймворк – это…

Фреймворк – слово, произошедшее от английского «framework» — остров, рама, структура. В программировании имеет несколько иное определение. Это программная платформа, отвечающая за определение структуры программной системы. Облегчает разработку, а также объединение компонентов имеющегося составленного кода.

Фреймворки – блоки заранее написанного программного кода, которые могут быть добавлены программистом для того, чтобы решать разнообразные задачи и проблемы. Включают в себя:

  • дополнительные утилиты;
  • библиотечные кодификации;
  • сценарии;

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

В основном фреймворки работают при помощи API. С их помощью удается проводить тест имеющейся кодификации. Особенно если речь идет о Java. Здесь данный вопрос решается несколькими способами – не только вручную, но и автоматически.

JUnit – определение

JUnit – специальная библиотека, необходимая для проведения модульного тестирования в Java. Портирована на разнообразные языки программирования. Активно используется разработчиками на практике.

Представляет собой фреймворк, который отвечает за реализацию отдельных «самостоятельных» элементов имеющейся кодификации. Помогает разработчикам при продумывании концепций собственных схем теста программного обеспечения.

JUnit представлена средой тестирования, обладающей открытым исходным кодом. Применяется для:

  • написания автоматического теста;
  • запуска соответствующей проверки.

Применяется и в качестве отдельной Джава-программы, и в IDE среде. Пример – Eclipse.

Ключевые особенности

Для того, чтобы проверить работоспособность программного кода, нужно провести тест. Вручную это делать не всегда удобно. Поэтому на помощь приходит JUnit. Он отвечает за обеспечение:

  • утверждения результатов, получаемых в ходе «анализа»;
  • тестовых функций, применяемых при обмене общими полученными показателями;
  • наборов тестов, способных облегчить организацию и запуск проверки;
  • графические и текстовые тесты бегунов.

Применяется на практике для того, чтобы осуществить проверку всего объекта, а также его части (методов или нескольких их групп, взаимодействующих между собой). Дополнительно JUnit проверяет слаженность и правильность работы нескольких объектов программного кода между собой.

Важно: с использованием JUnit можно значительно упростить проверку работоспособности имеющейся утилиты. Данный «инструмент» довольно легко использовать на практике.

Свойства JUnit

JUnit в Java имеет следующие свойства:

  • открытый исходный код;
  • ускорение написания итоговой кодификации (даже самой сложной);
  • поддержка аннотаций для того, чтобы можно было идентифицировать методы;
  • поддержка утверждений для тестов;
  • возможность организации тестов в их связке;
  • визуальная удобная индикация.

Пример последнего свойства можно увидеть, когда осуществляется непосредственный тест. Красным цветом выделяются текущие ошибки. Зеленым – те, что уже исправлены. Это очень удобно не только новичкам, но и продвинутым программерам.

Тестирование – каким бывает и при чем тут JUnit

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

  • тест черного ящика;
  • тест белого ящика.

В первом случае предстоит работать с учетом того, что внутренняя структура утилиты не принимается в расчет. Ключевой упор делается на функциональность, которую должен обеспечивать соответствующий контент. Если же говорить о тесте белого ящика, во внимание принимаются методы и классы внутреннего характера.

Также тесты условно разделяют на несколько уровней:

  • юнит – отдельные участки имеющегося кода;
  • интеграционный – проверка взаимодействия и совместной работы компонентов контента;
  • системный – тест всей системы в качестве единого целого;
  • прием – конечная проверка на соответствие заданным изначально требованиям.

Первый вариант – это всегда тестирование белого ящика. Других вариантов развития событий нет. JUnit здесь играет немаловажную роль. За счет него обеспечивается целостная проверка создаваемого контента.

Элементарный пример

Ниже представлен пример теста, проведенного в JUnit:

import org.junit.Test;
import junit.framework.Assert;
 
public class MathTest {
    @Test
    public void testEquals() {
        Assert.assertEquals(4, 2 + 2);
        Assert.assertTrue(4 == 2 + 2);
    }
 
    @Test
    public void testNotEquals() {
        Assert.assertFalse(5 == 2 + 2);
    }
}

Далее тест будет рассматриваться на примере версия JUnit 3 и JUnit4.

Версия JUnit 3

При определенных обстоятельствах требуется проводить итоговое тестирование через JUnit 3. Реализация предусматривает следующие нюансы:

  1. Нужно провести наследование тест-класса, который называется TestCase.
  2. Осуществить предопределение методов setup и tearDown, если это необходимо.
  3. Разработать тестовые методы, которые начинаются со слова «test».

Именно такие задачи будут рассматриваться в приведенном примере. После запуска тестов:

  • создается экземпляр тест-класса;
  • обрабатывается и выполняется setUp;
  • проводится запуск теста;
  • под конец реализовывается tearDown.

Когда тестовый метод влечет за собой исключение, вся проверка проваливается. Разработчик сможет увидеть ошибки, чтобы далее исправить их.

Тесты jUnit в Java включают в себя выполнение определенного кода, а также непосредственные проверки. Последние выполняются при помощи класса Assert. Иногда для их применения задействовано соответствующее ключевое слово.

Наглядный пример

Для того, чтобы лучше разбираться в выбранной теме, создадим утилиту, которая нужна для работы со строками. Она включает в себя:

  • методы проверки строчки на пустоту;
  • представления последовательности байт в шестнадцатеричном строчке.

Вот пример кода:

public class JUnit3StringUtilsTest extends TestCase {
  private final Map toHexStringData = new HashMap();

  protected void setUp() throws Exception {
      toHexStringData.put("", new byte[0]);
      toHexStringData.put("01020d112d7f", new byte[]{1,2,13,17,45,127});
      toHexStringData.put("00fff21180"  , new byte[]{0,-1,-14,17,-128 });
      //...
  }

  protected void tearDown() throws Exception {
      toHexStringData.clear();
  }

  public void testToHexString() {
      for (Iterator iterator = toHexStringData.keySet().iterator(); 
                                                     iterator.hasNext();)
      {
          final String expected = (String)iterator.next();
          final byte[] testData = (byte[])toHexStringData.get(expected);
          final String actual = StringUtils.toHexString(testData);
          assertEquals(expected, actual);
      }
  }
  //...
}

Дополнительные возможности

После выполнения теста можно смотреть на результат. Но сначала важно учесть то, что JUnit 3 имеет дополнительные возможности:

  1. Тесты могут быть сгруппированы. Для этого применяется отдельный класс под названием TestSuite.

2. Выполнение проверки разрешено повторять по несколько раз. Чтобы справиться с задачей, предстоит использовать RepetedTest.

3. Присутствует возможность проверки кода. Если ожидаете вывод результата относительно выброса исключения, нужно задать наследование тест-класса от ExceptionTestCase.

Приведенные примеры наглядно показывают – для проведения проверки в Джаве требуется минимум программного кода.

Продолжение статьи смотрите здесь.


Unit Testing

tutorial
java
junit
mockito


  • JUnit
  • Multiple Tests
  • Setup
  • Hard-coding
  • Mocks
    • Creating Mocks
    • Verification
    • Examining Arguments
  • Fixing Broken Tests
  • Adding Tests
  • Working in Small Steps
  • Other Types of Tests
    • Integration and UI Testing
    • Manual Testing

When writing code, it can be hard to know (let alone prove) that the code you wrote does the right thing. Does it handle input correctly? Does it fail gracefully when it’s given bad data? Does it deal with edge cases the way you expected?

You can manually test your code to increase your confidence, but that becomes more difficult when you have multiple people working on the same code: will the next person remember to test all of the same cases you originally tested? How will they know if they change your code in a way that breaks something?

Just as importantly, when you’re sharing a codebase with other people, how do you know that the code you wrote doesn’t break anything? Imagine working on a large codebase with hundreds of thousands of lines of code: how do you know that the little change you made to a library function won’t break everything?

This is where unit testing comes in handy. Unit tests are small programs that you write to test out your “real” code. A unit test generally only tests out one small piece of functionality, and you’ll usually write a bunch of unit tests to make sure your code handles a variety of conditions. By running unit tests on your code, you can be more confident that your change didn’t break anything.

JUnit

JUnit is a library that helps us write unit tests. JUnit handles stuff like automatically setting up an environment, calling a series of test functions, and making sure that the code does what we expected through assertions.

It’s probably easiest to just see an example. Consider this simple Java class:

public class ThingAdder {
 public int addThings(int one, int two) {
   return one + two;
 }
}

Here’s a JUnit test class that contains a single test function for the ThingAdder class:

import org.junit.Assert;
import org.junit.Test;

public class ThingAdderTest {

 @Test
 public void testAddThings(){
   ThingAdder thingAdder = new ThingAdder();
   int one = 1;
   int two = 2;
   
   int result = thingAdder.addThings(one, two);

   Assert.assertEquals(3, result);
 }
}

When we run this class through JUnit, JUnit sees the @Test annotation and automatically runs the testAddThings() function. This function creates some example input, calls the addThings() function, and then checks that the result is what we expected. If the Assert.assertEquals() function finds that the result is not what we expected, the test will fail. Otherwise, the test will pass.

Notice that the testAddThings() function is split up into three sections. The first section arranges the test by preparing example input, the second section acts by calling the function we’re trying to test, and the third section asserts to check that the result matches our expectations. Splitting your test code up into sections like this can help make it more obvious exactly what the test is doing.

Multiple Tests

So far we just have a single function that tests our ThingAdder class, but in real life we’d probably want multiple tests that check for different types of input, corner cases, and possible errors.

For example we might want a test for negative numbers, or for very large numbers.

import org.junit.Assert;
import org.junit.Test;

public class ThingAdderTest {

 @Test
 public void testAddThings_positive(){
   ThingAdder thingAdder = new ThingAdder();
   int one = 1;
   int two = 2;
   
   int result = thingAdder.addThings(one, two);

   Assert.assertEquals(3, result);
 }

 @Test
 public void testAddThings_negative(){
   ThingAdder thingAdder = new ThingAdder();
   int one = -1;
   int two = -2;
   
   int result = thingAdder.addThings(one, two);

   Assert.assertEquals(-3, result);
 }

 @Test
 public void testAddThings_overflow(){
   ThingAdder thingAdder = new ThingAdder();
   int one = Integer.MAX_VALUE;
   int two = 1;
   
   int result = thingAdder.addThings(one, two);

   Assert.assertEquals(Integer.MIN_VALUE, result);
 }

}

When we run JUnit, JUnit will run each of these test functions. Notice that each test function only tests one thing!

Writing tests also lets us test our own assumptions. For example, maybe we expected that overflow started over at 0 instead of at the minimum value. In that case, our test would have failed, and we would have been able to make the necessary change in our code.

Even with this simple example, we could probably add more tests: checking that a mix of positive and negative numbers works how we expect, checking that adding zero works how we expect, or checking that multiple calls to the addThings() function works how we expect. You’ll often end up writing more test code than “real” code!

Setup

Notice that each of our test functions starts with the same line of code:

ThingAdder thingAdder = new ThingAdder();

We can get rid of this duplication using the @Before annotation. Like its name suggests, any functions with this annotation will be run before each test function. This lets us move repeated initialization code from the test functions into a setup function:

import org.junit.Assert;
import org.junit.Test;

public class ThingAdderTest {

 ThingAdder thingAdder;

 @Before
 public void setup(){
   thingAdder = new ThingAdder();
 }

 @Test
 public void testAddThings_positive(){
   int one = 1;
   int two = 2;
   
   int result = thingAdder.addThings(one, two);

   Assert.assertEquals(3, result);
 }

 @Test
 public void testAddThings_negative(){
   int one = -1;
   int two = -2;
   
   int result = thingAdder.addThings(one, two);

   Assert.assertEquals(-3, result);
 }

 @Test
 public void testAddThings_overflow(){
   int one = Integer.MAX_VALUE;
   int two = 1;
   
   int result = thingAdder.addThings(one, two);

   Assert.assertEquals(Integer.MIN_VALUE, result);
 }

}

Now JUnit will automatically run the setup() function before it runs each test function.

Hard-coding

You’ve probably heard that using hard-coded values is generally a bad idea when we’re writing code. The opposite is true for test code! Tests should use hard-coded values as much as possible, and we should avoid putting any logic in our tests.

For example, note that we’re passing a value directly into the Assert.assertEquals() function. We might be tempted to do something like this instead:

Assert.asserEquals(one+two, thingAdder.addThings(one, two));

But this actually makes the test harder to read. It also just repeats the logic that we’re trying to test, so it’s not a very valuable test. Instead, we should hardcode values as much as possible:

Assert.asserEquals(42, thingAdder.addThings(one, two));

Mocks

Remember that a unit test should only test one thing at a time: a single class or a single function. But what if we have code like this?

public class ThingAdder{

 private DataConnection dataConnection;

 public ThingAdder(DataConnection dataConnection){
   this.dataConnection = dataConnection;
 }

 public void addThings() {
   int one = dataConnection.getThingOne();
   int two = dataConnection.getThingTwo();
   int result = one + two;
   dataConnection.setResult(result);
 }
}

This class uses another class named DataConnection to get and store data. How would we go about testing our ThingAdder class?

If DataConnection was a simple Java class, then our test class could create an instance of DataConnection and pass it into the ThingAdder constructor. But if DataConnection is more complicated and requires external dependencies like a database connection, then we’re better off using a mock. Mockito is a popular library for creating mocks.

A mock is an object that can be treated just like any other instance of a particular class, but without relying on any logic inside the class. Instead, we can tell our mock to return a certain value when a function is called.

Creating Mocks

To create a mock, we call the Mockito.mock() function and pass it the class we want to mock:

DataConnection mockDataConnection = Mockito.mock(DataConnection.class);

And then to mock out a function, we call the Mockito.when() and thenReturn() functions:

Mockito.when(mockDataConnection.getThingOne()).thenReturn(42);

Now if we call mockDataConnection.getThingOne(), the function will return 42 without invoking any internal logic.

Verification

To check whether a function is called on a mock object, we can use the Mockito.verify() function:

Mockito.verify(mockDataConnection.setResult(42));

This function will verify that our code called setResult(42) on our mock object, without invoking any of the logic inside the setResult() function. If our code does not call setResult(42), then the test will fail.

Putting it all together, our test would look like this:

import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;

public class ThingAdderTest {

 @Test
 public void testAddThings(){
   DataConnection mockDataConnection = Mockito.mock(DataConnection.class);
   Mockito.when(mockDataConnection.getThingOne()).thenReturn(1);
   Mockito.when(mockDataConnection.getThingTwo()).thenReturn(2);
   ThingAdder thingAdder = new ThingAdder(mockDataConnection);

   thingAdder.addThings();

   Mockito.verify(mockDataConnection.setResult(3));
 }
}

This test function creates a mock DataConnection, and mocks out its getThingOne() and getThingTwo() functions. It then verifies that the code called setResult(3) on the mock object. Of course, we could then add other test functions just like we did before.

Examining Arguments

If you want to take a more detailed look at the argument passed into a mock, you can use the ArgumentCaptor class. For example, imagine that we have a Result class and the DataConnection.setResult() function took a Result parameter. We could get at the value of that parameter like this:

ArgumentCaptor<Result> resultArgumentCaptor = ArgumentCaptor.forClass(Result.class);
Mockito.verify(mockDataConnection.setResult(resultArgumentCaptor.capture());
Result result = resultArgumentCaptor.getValue();
Assert.assertEquals(42, result.getResult());

This is just an example, and exactly what you do with the ArgumentCaptor will depend on what you’re trying to test.

Fixing Broken Tests

Unit tests confirm that your code works a certain way. So if you change the behavior of your code, your tests will break. So if you’re working in a project that contains unit tests, you’re going to spend a significant portion of your time fixing broken tests.

When a test fails, the first thing you should do is check whether it’s failing in a way you expected. Understand what the test was originally doing, and look at how your code changed the behavior being tested.

If the test did not break in a way that you expected, then you should take a closer look at your code to make sure it’s doing what you expected.

If the test did break in an expected way, then you should modify the test to reflect the new behavior.

For example, let’s say we modified our addThings() function to concatenate the values instead of use mathematical addition, so addThings(1, 2) returned 12 instead of 3, and addThings(123, 456) returned 123456 instead of 579. We would expect this change to break our tests: in fact, if it doesn’t break any tests, then our code isn’t doing what we expected it to do!

We’d then look at each failing test and make sure that the test is failing how we expected. Then we’d modify the tests to reflect the new behavior.

Adding Tests

In addition to modifying the existing tests, you’ll usually want to add tests for any new behavior in any code you write. Generally speaking, any pull request that contains a code change should also contain a test change.

In our example, we might add a new test for addThings(123, 456) or for very long numbers, or for values in different bases like binary or hexadecimal.

Writing code is only half the battle. You also have to add tests for the code you write!

Working in Small Steps

One of the most important things to remember is that you should not be submitting pull requests that contain hundreds of lines of code and thousands of lines of tests. For the sake of the sanity of both you and your coworkers, try to work in small pieces!

Try to break your problem down into smaller individual steps, and then take those steps on one at a time. This will make it easier to write tests, and your code will be easier for your coworkers to review. Try to get into the habit of going through this process:

  • Write the code for one small piece of the problem.
  • Fix broken tests.
  • Add new tests.
  • Send it off for code review!

Other Types of Tests

Unit tests are designed to be small and to only test one thing at a time. This is great for verifying that individual pieces of your code work how you expect and don’t break in the future, but they aren’t great for testing a full system end to end or for testing out a user interface.

Integration and UI Testing

End-to-end tests are called integration tests and usually involve more elaborate test frameworks. For example, an integration test might start up a test server and then programmatically use the mouse and keyboard to test that the web browser does the right thing.

Manual Testing

Unit tests are a form of automated testing, which means that the tests run automatically and code is ultimately responsible for determining whether something is working correctly. These types of tests are good for detecting when a change in the code breaks an assumption made somewhere else in the code, but they’re not a substitute for manually testing that everything works yourself.

In addition to writing tests, always make sure that the code actually works the way you expect it to. Run a local server, and click around the site as if you’re a user. Test to make sure that the previous functionality still works, and that whatever you added does what you expected it to.

It’s fine to have unfinished features in your code: in fact, it’s a very good idea to break things down into small pieces and submit those pieces one at a time instead of waiting until a whole feature is done. But each of those changes should be complete enough so that it doesn’t break the rest of project! The code should compile, and everything should still work without breaking.

It can be useful to come up with a checklist and run through it whenever you’re ready to submit a change.

Понравилась статья? Поделить с друзьями:
  • Как написать тест план
  • Как написать тест на делфи
  • Как написать тест на python
  • Как написать тест на html
  • Как написать тест кейс пример