Есть ли лучший способ написания модульных тестов, чем серия «AssertEquals»?

12

Вот базовый пример того, каким должен быть мой модульный тест с использованием qunit:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Insert title here</title>

<link rel="stylesheet" href="qunit/qunit-1.13.0.css">
<script src = "qunit/qunit-1.13.0.js"></script>
<script src = "../js/fuzzQuery.js"></script>

<script>

test("Fuzz Query Basics", function()
        {
            equal(fuzzQuery("name:(John Smith)"), "name:(John~ Smith~)");
            equal(fuzzQuery("name:Jon~0.1"), "name:Jon~0.1");
            equal(fuzzQuery("Jon"), "Jon~");
            //etc

        }
    );

</script>
</head>
<body>
    <div id="qunit"></div>
</body>
</html>

Теперь я думал, что это немного повторяется.

Может поместить все входы / выходы в массив и пройти через него.

test("Fuzz Query Basics", function()
        {
            var equals = [
                           ["name:(John Smith)", "name:(John~ Smith~)"],
                           ["name:Jon~0.1", "name:Jon~0.1"],
                           ["Jon", "Jon~"]
                           ];

            for (var i = 0; i<equals.length; i++)
                {
                    equal(fuzzQuery(equals[i][0]), equals[i][1]);               
                }

        }
    );

И это прекрасно работает.

Единственное преимущество, которое я могу придумать для этого второго метода, заключается в том, что, если выясняется, что вы на самом деле не хотите использовать equal, проще сделать это изменение в одном месте.

С точки зрения читабельности, я не думаю, что это так или иначе, но я, вероятно, предпочитаю второе.

Абстрагируя его дальше, вы можете поместить случаи ввода / вывода в отдельный файл CSV, что может упростить его изменение.

Вопрос в том, каковы общие правила написания подобных тестов?

Есть ли причина, по которой вы не должны помещать их в массивы?

dwjohnston
источник
Будет ли один из них сказать вам, какое значение не удалось?
Джеффо
1
@JeffO - Да - С QUnit - Если тест не пройден, выходные данные покажут ожидаемое и фактическое значение.
dwjohnston

Ответы:

8

Ваши рефакторированные тесты имеют запах: логика условных тестов .

Причины, по которым вам следует избегать написания условной логики в ваших тестах, двояки. Во-первых, это подрывает вашу способность быть уверенным в правильности тестового кода, как описано в связанной статье xUnit Patterns.

Во-вторых, это заслоняет смысл тестов. Мы пишем методы тестирования, потому что они помещают логику для тестирования определенного поведения в одном месте и позволяют нам дать ему описательное имя (см . Оригинальную статью Дэна Норта о BDD для изучения значения хороших имен для тестов). Когда ваши тесты скрыты внутри одной функции с forциклом, это скрывает смысл кода для читателя. Мало того, что читатель должен понимать цикл, он также должен мысленно разгадать все различные поведения, тестируемые в цикле.

Решение, как всегда, состоит в том, чтобы подняться на уровень абстракции. Используйте тестовый фреймворк, который дает вам параметризованные тесты , как это делают xUnit.NET или Contexts (заявление об отказе: я написал Contexts). Это позволяет группировать триангуляционные тесты для одного и того же поведения естественным образом, сохраняя при этом тесты для отдельных поведений раздельными.

Бенджамин Ходжсон
источник
Хороший вопрос, кстати
Бенджамин Ходжсон
1
1) Если вы поднимаетесь на уровень абстракции, не скрываете ли вы те самые детали, которые, как вы сказали, скрываются циклом for? 2) не уверены, что параметризованные тесты применимы здесь. Кажется, что где-то здесь есть параллели, но у меня было множество ситуаций, похожих на OP, где у меня был набор данных из 10-20 значений, и я просто хотел провести все из них через SUT. Да, каждое значение различно и потенциально может тестировать разные границы, но, похоже, на самом деле «придумывать» имена тестов для каждого отдельного значения было бы излишним. Я нашел оптимальное соотношение значение / размер кода при использовании аналогичного ...
ДХМ
... петли. Пока тест не пройден, assert печатает именно то, что не удалось, у разработчика достаточно обратной связи, чтобы точно определить проблему.
ДХМ
@DXM 1) тестовая среда обеспечивает параметризованную функциональность теста. Мы полностью доверяем тестовой среде, поэтому не пишем для нее тесты. 2) параметризованные тесты предназначены именно для этой цели: каждый раз вы выполняете одни и те же шаги, но с разными значениями ввода / вывода. Тестовая среда избавляет вас от необходимости писать имена для каждого, выполняя различные входные данные с помощью одного и того же метода тестирования.
Бенджамин Ходжсон
5

Кажется, вы действительно хотите модульный тест, управляемый данными. Поскольку вы упомянули об использовании QUnit, я обнаружил плагин, который позволяет параметризованные тесты:

https://github.com/AStepaniuk/qunit-parameterize

В тесте, управляемом данными, нет ничего идеологического, если сам код теста не является условным. Глядя на ваш тестовый код, он кажется очень хорошим кандидатом на тест на основе данных.

Пример кода для GitHub README:

QUnit
    .cases([
        { a : 2, b : 2, expectedSum : 4 },
        { a : 5, b : 5, expectedSum : 10 },
        { a : 40, b : 2, expectedSum : 42 }
    ])
    .test("Sum test", function(params) {
        var actualSum = sum(params.a, params.b);
        equal(actualSum, params.expectedSum);
    });
Грег Бургардт
источник
1
Согласитесь, это похоже на тест на основе данных. Но, похоже, это то, что он уже имеет во втором примере кода.
Роберт Харви
1
@RobertHarvey - Правильно. Есть общепринятый термин для того, что он пытается выполнить, и существует плагин для используемой среды тестирования, чтобы облегчить написание таких тестов. Я думал, что стоит отметить ответ на будущее, вот и все.
Грег Бургхардт
1

Вы повторяете себя меньше, используя массив, который более удобен в обслуживании. Один из подходов, который мне нравится использовать, - это иметь отдельный метод, который организует, выполняет и утверждает тесты, но принимает входные параметры, с которыми я тестирую, поэтому у меня есть 1 метод тестирования на каждый набор входов.

Это позволяет мне мгновенно определить, какие тесты / входные данные дают сбой.

Kevin
источник
0

Мне нравится ваш второй подход, но я бы добавил 2 балла

  • не используйте массивы для хранения проверенных данных, так как работа с индексами не является чистым способом
  • не используйте forпетли

`

[
    {
        process: "name:(John Smith)",
        result: "name:(John~ Smith~)"
    },
    {
        process: "name:Jon~0.1", 
        result: "name:Jon~0.1"
    },
    {
        process: "Jon", 
        result: "Jon~"
    }
]
.forEach(function(data){

    var result = fuzzQuery(data.process);
    equal(result, data.result);
});

Я не уверен насчет qunit, но хороший тестировщик покажет вам, какая строка ввода не удалась, и каков был ожидаемый результат

tenbits
источник