пятница, 21 июня 2019 г.

Мутационные тесты на страже нашего кода

Привет, мой брат-программист!
Не спится? Опять твои модульные тесты пропустили зловредный баг? Мне знакома твоя боль... И сегодня я хочу подарить тебе лекарство, которое, хоть и не излечит болезнь полностью, но устранит самые неприятные из ее симптомов.

Не веришь своим ушам? Всего два слова, всего два - и твои модульные тесты станут надежными, как Форт Нокс и полными, как Тихий Океан.
Повторяй за мной: мутационные тесты!



Зачем нужны тесты тестов

Если словосочетание "мутационные тесты" ты слышишь впервые, то не пугайся, я кратко введу тебя в курс дела. 
Есть программы, которые пишут программисты. Программисты пишут программы с ошибками и это - факт. Программисты хотят, чтобы этих ошибок было как можно меньше и это - тоже факт. 
Для того, чтобы уменьшить количество ошибок в программах, программисты пишут другие программы, которые тестируют первые программы. Эти тестирующие программы называются модульными тестами, о них я рассказывал тут (есть еще много разных тестов, но поговорим пока о модульных).
Поскольку модульные тесты - тоже программы и их тоже пишут те же самые программисты - в них тоже есть ошибки. Но это ошибки другого рода - это ошибки тестирования первоначальных программ. 
Соответственно, нам нужны еще программы, которые тестируют программы, тестирующие наши программы. Одним из видов таких программ, тестирующих тесты, являются мутационные тесты.
Мутационные тесты исходят из того, что если изменить код исходной программы, тесты это "заметят". 
Если же тесты не замечают этого изменения, это - плохие тесты и их нужно срочно исправлять. 

Приведу простой пример: допустим, у нас есть класс Add, который отвечает за сложение двух целых чисел:
package com.blogspot.toolkas;

public class Add {
 public int process(int val1, int val2) {
  return val1 + val2;
 }
}
Метод процесс кажется нам довольно тривиальным, поэтому мы, поленившись, пишем такой тест:
package com.blogspot.toolkas;

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

public class AddTest {
    @Test
    public void testProcess() {
        Add add = new Add();

        Assert.assertEquals(0, add.process(0, 0));
    }
}
Является ли такой тест хорошим? Очевидно, нет.
И мутационные тесты должны нам это продемонстрировать: например, мутационный тест может поменять знак сложения в исходном коде на знак вычитания:
package com.blogspot.toolkas;
public class Add {
 public int process(int val1, int val2) {
  return val1 - val2;
 }
}
Однако, даже после такого изменения, тест AddTest не обнаружит ошибки, что означает, что тест неполон и нужно его изменить.
Как видим, принцип работы мутационных тестов прост и довольно прямолинеен.

PiTest - мутационные тесты в действии

Возможности мутационных тестов я продемонстрирую на примере библиотеки PiTest в контексте экосистемы Maven. Для этого создадим простой проект:
pitest-example
└─ src
│  ├─ main
│  │   └─ java
│  │      └─ com
│  │         └─ blogspot
│  │            └─ toolkas
│  │               └─ Add.java
│  └─ test
│      └─ java
│         └─ com
│            └─ blogspot
│               └─ toolkas
│                  └─ AddTest.java
└─ pom.xml
В pom.xml у нас только одна зависимость - библиотека junit:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.blogspot.toolkas</groupId>
    <artifactId>pitest-example</artifactId>
    <version>1.0-SNAPSHOT</version>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
Для PiTest существует Maven-плагин, поэтому для запуска мутационных тестов достаточно в папке проекта выполнить команду:
mvn clean package org.pitest:pitest-maven:mutationCoverage
После завершения работы плагина, нам нужно ознакомиться с отчетом PiTest, который лежит примерно тут: target/pit-reports/201906212122/index.html


Если провалиться по ссылке com.blogspot.toolkas, а потом провалиться в отчет класса Add.java, то можно увидеть следующее:

Строчка
1. Replaced integer addition with subtraction → SURVIVED
говорит нам, что PiTest заменил знак сложения на знак вычитания, но тест AddTest был пройден успешно, что говорит о том, что тест неполон.

К счастью, ситуацию исправить довольно легко - нужно лишь добавить больше разнообразных проверок:
package com.blogspot.toolkas;

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

public class AddTest {
    @Test
    public void testProcess() {
        Add add = new Add();

        Assert.assertEquals(1, add.process(1, 0));
        Assert.assertEquals(1, add.process(0, 1));
        Assert.assertEquals(0, add.process(11, -11));
        Assert.assertEquals(20, add.process(1, 19));
    }
}
В таком случае отчет PiTest будет выглядеть так:


что означает, что все мутации PiTest повлияли на AddTest и это хорошо.

Заключение

На этом простом примере я постарался продемонстрировать ту пользу, которую принесет вам систематическое использование мутационных тестов. 
Ваши модульные тесты станут надежнее, ошибок - меньше, а код - безопаснее.
Ведь каждый, кто смотрел черепашек-ниндзя, точно знает: мутации - ключ к победе!



Комментариев нет:

Отправить комментарий