Top.Mail.Ru

Dynamics 365. Основные рекомендации написания тестов (Acceptance Test Library) Х++

После появления механизма тестирования Acceptance Test Library (ATL) в Dynamics 365 можно выделить основные рекомендации по написанию тестов. Правильно написанный класс для тестирования, позволит получить максимальную пользу.

Мы пишем модульные тесты для:

  • Автоматического обнаружения регрессий

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

  • Упрощения внесения изменений

    Автоматические тесты позволяют производить рефакторинг кода и оставаться уверенным, что все по-прежнему работает правильно.

  • Получения лучшего дизайна и интерфейсов

    Отличный побочный продукт Test-Driven-Development (разработка через тестирование).

  • Документирования поведений и интерфейсов

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

К сожалению, автоматическое тестирование по своей природе не дает нам всех этих преимуществ. Создание тестов — это отдельная дисциплина и к ней следует подходить с таким же профессионализмом, как и при написании качественного рабочего кода. Для автоматизированных тестов рассмотрим 5 качеств (и их анти-качеств). Если удастся написать тесты, соответствующие этим качествам, вы будете намного ближе к цели.

Качества

Надежность

Поддерживаемость

Высокая скорость

Простота

Точность

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

 

Анти-качества

Ненадежность

Не поддерживаемые

Низкая скорость

Сложность

Неточность

Противоречивые результаты.

 

Тесты зависят от общего состояния системы.

Длинные тестовые методы.

 

Дублирование кода в тестах.

Тесты с невнятным смыслом.

Тесты долго выполняются.

 

Создание большого количества данных.

Использование условной логики в тестах.

 

Сложный дизайн теста.

Применение нескольких типов поведения.

 

Необходима сложная настройка.

Надежность

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

Тесты могут стать ненадежными из-за утечки. Это может оказаться трудно отследить, так как один тест может пройти, а последующий не пройти. В X ++ обычно бывает два типа утечки: база данных и память. Механизм выполнения тестов многое делает для сброса состояний между тестами, включая восстановление моментального снимка базы данных между выполнением каждого метода тестирования и очистку глобальных кэшей.

Распространенная причина нестабильности теста — это когда тест использует один экземпляр класса для нескольких методов тестирования. Это означает, что любые члены уровня класса сохраняют свои значения и, таким образом, могут влиять на последующие тесты в том же классе. Иногда последующий тест даже зависит от состояния утечки, что приводит к сбою при индивидуальном запуске. Методы тестирования должны работать в любой последовательности и индивидуально.

[SysTestCaseUseSingleInstance]
public class MyTestClass extends SysTestCase
{ }

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

[SysTestMethod]
public void transactionCreatedDateTimeIsRight()
{
    var expectedTime = DateTimeUtil::getSystemDateTime();
    transaction.insert();
    this.assertEqual(expectedTime, transaction.createdDateTime, "Transaction time is wrong");
}

Поддеживаемость

Тесты требуют минимальных усилий или совсем не требуют для обновления при изменении системы. Часто путают поддерживаемость и надежность. Эти две концепции совершенно разные. Надежность говорит о поведении теста при отсутствии изменений, в то время как поддерживаемость говорит о том, насколько хорошо тест справляется с изменениями в системе.

Работа с infolog-сообщениями в тестах может быть сложной задачей и хорошим примером того, что нужно проверять. В большинстве случаев тесты не должны заботиться о фактических сообщениях. Предположим, проверять содержимое конкретного сообщения — должен ли тест тогда завершится с ошибкой?

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

[SysTestCaseUseSingleInstance]
public class MyTestClass extends SysTestCase
{ }

Ниже пример модульного теста, проверяющего, что используется конкретная метка. Этот тест затруднит изменение того, какая метка используется в системе. Тест не должен заботиться о том, какая метка используется, а должен проверить наличие метки.

[SysTestMethod]
public void captionHasRightLabel() 
{ 
    this.assertEquals("@SYS423123", myClass.caption(), "Wrong label identifier is used"); 
}

В общем шаблоне Arrange/Act/Assert заставляет явно указывать, что тестируется, а что утверждается. Начинайте каждый тест с добавления 3 комментариев, а затем пишите код.

[SysTestMethod]
public void casingInvariant()
{
    InventDim inventDim, newInventDim;

    ttsbegin;

    // Arrange
    inventDim.LicensePlateId   = 'LP1';
    inventDim.InventStatusId   = 'Status1';

    newInventDim.LicensePlateId   = 'lp1';
    newInventDim.InventStatusId   = 'status1';

    //Act
    inventDim    = InventDim::findOrCreate(inventDim);
    newInventDim = InventDim::findOrCreate(newInventDim);

    //Assert
    this.assertEquals(inventDim.RecId, newInventDim.RecId, "InventDim records should match");

    ttsabort;
}

Если намерение остается очевидным, группы можно свернуть.

[SysTestMethod]
public void valueNullIsEmptyString() 
{ 
    // Arrange/Act 
    str returnValue = SysQuery::value(null); 
   
    // Assert 
    this.assertEquals('', returnValue, "Null is not handled correctly"); 
}

Производительность

Своевременное получение результатов увеличивает ценность тестирования. В идеале все тесты могут быть запущены локально в течение нескольких минут. Таким образом, регрессия может быть обнаружена до того, как код будет опубликован. Для достижения этой цели убедитесь, что все модульные тесты выполняются менее чем за 1 секунду, а тесты компонентов — менее чем за 20 секунд.

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

  • Избегайте ненужной настройки

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

  • Сведите к минимуму использование адаптеров формы

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

  • Минимизируйте зависимости

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

  • Избегайте избыточных тестов

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

Простота

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

Взгляните ниже на пример слишком сложного теста:

[SysTestMethod]
public void contributionConstantPerUnitIsCorrect()  
{  
    // Arrange 
    CostTmpSheetCalcResult costTmpSheetCalcResult;      
    
    costTmpSheetCalcResult.ContributionConstant = 5.00;  
    costTmpSheetCalcResult.Qty = 1; 
    costTmpSheetCalcResult.IsHeader = NoYes::Yes; 
    costTmpSheetCalcResult.Level  = 10; 
    costTmpSheetCalcResult.doInsert();  
 
    // Act/Assert  
    this.assertEquals(5.00, costTmpSheetCalcResult.contributionConstantPerUnit(), "The calculation of constant contribution per unit is incorrect."); 
}

В нем имеется несколько проблем:

  1. Некоторые настройки не нужны. Хотя сделать их можно быстро, но это отвлекает от основной цели теста.
  2. Присутствуют «магические» числа, связи с которыми не очевидны.
  3. Одно из «магических» чисел может снизить вероятность обнаружения регрессии.

Стоит переписать тест:

[SysTestMethod]
public void contributionConstantPerUnitIsCorrect()  
{  
    // Arrange  
    CostTmpSheetCalcResult costTmpSheetCalcResult;  
    const var contributionConstant = 5;
    const var qty = 2;

    costTmpSheetCalcResult.ContributionConstant = contributionConstant;  
    costTmpSheetCalcResult.Qty = qty; 
    costTmpSheetCalcResult.doInsert();  
 
    // Act/Assert  
    this.assertEquals(qty * contributionConstant, costTmpSheetCalcResult.contributionConstantPerUnit(), "The calculation of constant contribution per unit is incorrect."); 
}

Точность

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

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

[SysTestMethod]
public void newFromIdentifierReturnsValidInstance() 
{ 
    MeasureTypeIdentifier identifier = classStr(MeasureTypeTestable); 
    MeasureType measureType = MeasureTypeFactory::newFromIdentifier(identifier); 
     
    this.assertNotNull(measureType, "Instance not created"); 
    this.assertTrue(measureType is MeasureTypeTestable, "Incorrect instance created"); 
}

Сравните это с тестом ниже. Это могло произойти так:

«Тест testContributionAndCostMethods () завершился неудачно. (Ожидаемое значение: 10, фактическое: 11. «Тест 4»)».

[SysTestMethod]
void testContributionAndCostMethods() 
{ 
    CostTmpSheetCalcResult costTmpSheetCalcResult; 
 
    costTmpSheetCalcResult.clear(); 
    costTmpSheetCalcResult.NodeCode = 'Code'; 
    costTmpSheetCalcResult.NodeDescription = 'Description'; 
    costTmpSheetCalcResult.ContributionVariable = 20.00; 
    costTmpSheetCalcResult.ContributionConstant = 10.00; 
    costTmpSheetCalcResult.CostVariable = 22; 
    costTmpSheetCalcResult.CostFixed = 8; 
    costTmpSheetCalcResult.Level = 1; 
    costTmpSheetCalcResult.Qty = 2; 
    costTmpSheetCalcResult.IsHeader = NoYes::Yes; 
    costTmpSheetCalcResult.IsTotal = NoYes::Yes; 
    costTmpSheetCalcResult.doInsert(); 
 
    select firstonly costTmpSheetCalcResult; 
 
    this.assertEquals(5.00, costTmpSheetCalcResult.contributionConstantPerUnit(), "Test 1"); 
    this.assertEquals(30.00, costTmpSheetCalcResult.contributionTotal(), "Test 2"); 
    this.assertEquals(15.00, costTmpSheetCalcResult.contributionTotalPerUnit(), "Test 3"); 
    this.assertEquals(10.00, costTmpSheetCalcResult.contributionVariablePerUnit(), "Test 4"); 
    this.assertEquals(4.00, costTmpSheetCalcResult.costFixedPerUnit(), "Test 5"); 
    this.assertEquals(30.00, costTmpSheetCalcResult.costTotal(), "Test 6"); 
    this.assertEquals(15.00, costTmpSheetCalcResult.costTotalPerUnit(), "Test 7"); 
    this.assertEquals(11.00, costTmpSheetCalcResult.costVariablePerUnit(), "Test 8"); 
}

microsoft.com

Comments

So empty here ... leave a comment!

Добавить комментарий

Sidebar