Pull to refresh

Data-Driven тесты в MS-Test для модульного и приёмочного тестирования

Reading time 6 min
Views 16K
Я бы сразу хотел подчеркнуть тот факт, что мир модульных тестов и мир приёмочных тестов через пользовательский интерфейс – это очень разные миры: со своими законами, разными возможностями и ограничениями. И если мир модульных тестов работает на покрытие каждой части приложения по отдельности и в изоляции, то тесты через пользовательский интерфейс – это эмуляция работы пользователя с системой, по большей части через нажатие кнопок и набор текста, которые в итоге сливаются в более крупные бизнес-сценарии.

Не редко оказывается так, что даже если инструмент предоставляет очень хорошие возможности для модульных тестов – то эти возможности оказываются практически неприменимы для UI-тестов.

Так случилось и в моей практике, когда я решил использовать data-driven тесты в фреймворке Ms-Test. В этой статье я более детально опишу проблему и свое решение, за которое до сих пор не пойму – мне нужно гордиться или стыдиться.

Data-Driven подход в мире модульных тестов


Предположим, у нас есть метод int ConvertToNumber(string input), который на вход принимает строку с числом и возвращает это же число, только в целочисленном типе.

Пусть мы напишем один позитивный тест, (который приносит радость) например, для input = “3” и проверим, что результат будет тоже 3.

Что еще? Конечно же, граничные значения и классы эквивалентности: “-1”, “0”, “MaxInt”, “-(MaxInt)”.
Хорошо, а что если на вход попадет десятичная дробь? А если строка начинается с цифры, за которой следуют символы?

Каково должно быть правильное решение в таких разных ситуациях?
А правильного решения – нет.

Обратите внимание, что разные языки программирования обрабатывают такие ситуации по-разному. Например, JavaScript в результате операции { “10” + 1} вернет строку “101”, а в результате { “10” — 1} будет число 9. А Perl скажет: 11 и 9 соответственно. Но, отсутствие «истинного» ответа совсем не мешает нам самостоятельно придумать правильные варианты поведения и сказать, что отныне – это есмь истина, и высечь эту истину модульными тестами в камне.

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

Пример data-driven модульного теста на Ms-Test


Для наглядности примера, в реализации ConvertToNumber, я не буду использовать Convert.ToInt32() –это было бы слишком просто. Вместо этого, предложу свою реализацию:
static int ConvertToNumber(string input)
{
    int result = 0;
    while (input.Length > 0)
        result = (input[0] >= '0'
        || input[0] <= '9') ? (result
        * 10) + input[0] - '0' + (((input
        = input.Remove(0, 1)).Length
        > 0) ? 0 : 0) : 0;
    return result;
}

К сожалению, в Ms-Test пока нет возможности задать входящие и ожидаемые значения непосредственно в коде, при помощи атрибутов. Зато, есть альтернатива – использовать источники данных. И в качестве такого источника, вы не поверите, будем использовать Excel-файл.

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



Главное – не забыть выделить нужный фрагмент и форматировать его как «Таблица с заголовком». Иначе – магия не будет работать.

Тогда модульный тест с реализаций соединения с Excel-таблицей и чтением данных будет выглядеть следующим образом:

const string dataDriver = "System.Data.OleDb";
const string connectionStr = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=C:\\matrix.xlsx;Extended Properties=\"Excel 12.0 Xml;HDR=YES\";";

[TestMethod]
[DataSource(dataDriver, connectionStr, "Shit1$", DataAccessMethod.Sequential)]
public void TestMe()
{
    string rowInput     = TestContext.DataRow["Input"].ToString();
    string rowExpected  = TestContext.DataRow["Expected Result"].ToString();
    string rowException = TestContext.DataRow["Exception"].ToString();
    string rowComment   = TestContext.DataRow["Comment"].ToString();

    int actualResult = ConvertToNumber(rowInput);
    Assert.AreEqual(rowExpected, actualResult.ToString());
}


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



А чтобы добавить новый тест – достаточно добавить новую строку в Excel. И в код лезть не нужно, чтобы посмотреть, что же конкретно тестами покрыто, а что – нет.

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

Прошу заметить, что даже на моей низко производительной машине, все 14 тестов прошли за менее чем за 1 секунду.

Не знаю как вы, но я был просто впечатлен такими возможностями и наглядными результатами.
Примечание: Для того, чтобы всё заработало, не забудьте скопировать файл matrix.xlsx в корень диска C:\

Data-Driven подход в мире тестирования через пользовательский интерфейс


К огромному моему сожалению, после того, как я взялся за UI-тесты с использованием Selenium WebDriver – моему восхищению от мега-крутой фичи Ms-Test пришёл конец.

Дело в том, что UI-тесты в любой реализации, по своей природе очень медленные, по сравнению с модульными. Тут нельзя просто так взять и вызвать функцию из ядра системы, с легкостью жонглируя контекстом и параметрами. Нет… для того, чтобы реализовать сложный пользовательский сценарий – нужно накликать множество кнопок.

Следовательно, с учетом всех возможных оптимизаций, один UI-тест может идти от 30-ти секунд до десятков минут. Всё зависит от сложности сценария.

Например, если предположить, что 1 из 14 data-driven тестов идет одну минуту – то весь набор пройдет за 14 минут.
А теперь представьте себе, что один тест упадёт по какой-то причине, например из-за того, что кнопочка на странице не успела появиться…
Не беда, скажете вы, ведь можно исправить и прогнать только упавшие тесты.
Нет. Ms-Test считает пачку data-driven тестов как один тест. Следовательно, чтобы перепрогнать 1 тест – придётся запускать все 14 вновь. И не факт, что те тесты, которые проходили раньше – вдруг не упадут. А в случае отладки, придётся не просто ставить точку останова, а настраивать условную точку останова: Visual Studio это позволяет… но, это требует дополнительных усилий и затрат времени.

Нужно было найти способ, который бы позволил перепрогнать только упавший тест. И я такой способ нашел.

Решение задачи: «цикл через наследование»


Для решения задачи, необходимо добавить в проект 2 файла: TestBase.cs и TestRows.cs.

Тогда в пространстве имён “MsTestRows.Rows.*”, появятся классы TestRows_01 … TestRows_100, которые содержат в себе от одного до ста генерированных методов.

Унаследуйте ваш TestClass от TestRows_NN с необходимым количеством методов (NN).

Далее, Visual Studio попросит реализовать два метода:
  • GetNextDataRow() – который будет возвращать данные для следующего теста
  • TestMethod() – который будет вызван из теста с данными, полученными от GetNextDataRow().

Полный код примера
using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace HabraDrivenTests
{

    // Custom Data Row 
    public class HabrDataRow
    {
        public string Input { get; set; }
        public int Expected { get; set; }
        public Type ExpectedException { get; set; }
        public string Comment { get; set; }

    }
    
    [TestClass]
    public class HabrTestInherienceLoop : MsTestRows.Rows.TestRows_04<HabrDataRow>
    {
        // Production-ready method
        static int ConvertToNumber(string input)
        {
            int result = 0;
            while (input.Length > 0)
                result = (input[0] >= '0'
                || input[0] <= '9') ? (result
                * 10) + input[0] - '0' + (((input
                = input.Remove(0, 1)).Length
                > 0) ? 0 : 0) : 0;
            return result;
        }

        // Test Data
        static HabrDataRow[] testData = new HabrDataRow[]
        {
            #region Data
            new HabrDataRow()
            {
                 Input    = "0",
                 Expected = 0,
                 ExpectedException = null,
                 Comment = "Граничное значение",
            },
            new HabrDataRow()
            {
                 Input    = "1",
                 Expected = 1,
                 ExpectedException = null,
                 Comment = "Граничное значение",
            },
            new HabrDataRow()
            {
                 Input    = "-1",
                 Expected = -1,
                 ExpectedException = null,
                 Comment = "Граничное значение",
            },

            new HabrDataRow()
            {
                 Input    = "2147483647",
                 Expected = 2147483647,
                 ExpectedException = null,
                 Comment = "int.MaxValue",
            },
            #endregion
        };

        // Data Generator
        public override HabrDataRow GetNextDataRow(int rowIndex)
        {
            return testData[rowIndex];
        }

        // Test Implementation
        public override void TestMethod(HabrDataRow dataRow, int rowIndex)
        {
            int actualResult = ConvertToNumber(dataRow.Input);
            Assert.AreEqual(dataRow.Expected, actualResult);
        }
    }
}


В результате, воспользовавшись таким извращенным маневром, мне удалось добиться возможности перезапустить только упавший тест.
image

Исходный код и ссылки



Другие материалы по теме:



Примечание 1: В файле TestRows.cs – 30 000 строк кода. Если ваш зарплатный бонус зависит от количества написанных строк кода, и система автоматически начислит вам бонус в размере 1 миллиона долларов, то я думаю, будет справедливо, если вы вышлете мне хотя бы 5% от этой суммы.

Примечание 2: Попробуйте реализовать ConvertToNumber так, чтобы он проходил все тесты.
Внимание: придерживайтесь “авторского стиля”.
Tags:
Hubs:
+6
Comments 1
Comments Comments 1

Articles