Создайте новый код для описания функциональности пользователя

Являетесь новым пользователем Visual Studio Application Lifecycle Management (ALM) или Team Foundation Server? Хотите узнать, как эффективнее всего работать с их новыми версиями для создания приложений?

Тогда потратьте несколько минут, чтобы изучить пошаговое руководство, которое приводится в двух главах этого учебника. В нем описывается один день из жизни Питера и Джулии — двух разработчиков из условной компании Fabrikam Fiber, которая занимается кабельным телевидением и сопутствующими сервисами. На примерах из этого руководства вы разберетесь, как при помощи Visual Studio и TFS извлекать и обновлять код, приостанавливать работу, если необходимо отвлечься на что-то другое, запрашивать проверку кода, возвращать изменения и выполнять другие задачи.

С чего все начиналось

Недавно в команде начали переходить на Visual Studio и Team Foundation Server для управления жизненным циклом приложений (ALM). Они настроили сервер и клиентские машины, определили невыполненную работу, составили план итерации и спланировали все остальное, без чего нельзя приступать к разработке.

Краткое описание главы

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

Примечание

Функции "Моя работа" и "Покрытие кода", которые рассматриваются в этом разделе, доступны только в Visual Studio Premium и Visual Studio Ultimate.

В этом разделе

  • Просмотр личного списка невыполненных работ и подготовка задач для начала работы

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

  • Создание заглушки для нового кода

  • Выполнение первого теста

  • Согласование интерфейса

  • Красный, зеленый, рефакторинг…

  • Покрытие кода

  • Ожидаемый результат

  • Возврат изменений

Просмотр личного списка невыполненных работ и подготовка задач для начала работы

В Team Explorer Питер открывает страницу Моя работа. Вся команда согласилась с тем, что в текущем спринте Питер будет работать над состоянием счета "Оценка" — важнейшим пунктом в списке невыполненных работ по продукту. А приняться за работу Питер решил с главного этапа этой задачи — реализации математических функций. Первым делом он перетащил эту задачу из списка Доступные рабочие элементы в список Выполняемая работа и изменения.

Просмотр личного списка невыполненных работ и подготовка задач для начала работы

Список задач на странице "Моя работа" в Team Navigator

  1. В командном обозревателе выполните следующее.

    1. Подключитесь к командному проекту, в котором вы планируете работать, если вы не сделали этого ранее.

    2. Нажмите значок Значок "Начало" Домой и выберите пункт Значок "Моя работа" Моя работа.

  2. На странице Моя работа перетащите нужную задачу из списка Доступные рабочие элементы в список Выполняемая работа.

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

Составление пошагового плана работ

Чаще всего Питер дробит написание кода на мелкие этапы. Каждый из них обычно занимает не более часа — а иногда всего 10 минут. На каждом этапе он записывает новый модульный тест и меняет код, который он разрабатывает. Код должен проходить все тесты: и новый, и ранее созданные. Иногда он сначала пишет тест, а потом меняет код, а иногда наоборот. Время от времени он выполняет рефакторинг, то есть просто улучшает код без добавления тестов. Если код успешно проходит тесты, Питер никогда не меняет их, разве что если решит, что они не вполне отражают требования.

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

Однако пока все задачи не будут завершены, Питер не возвращает код в Team Foundation Server.

Питер составляет черновой план для этой последовательности этапов. Он знает, что по мере работы конкретные детали и порядок этапов наверняка изменятся. Вот список этапов, который он изначально составил для этой задачи:

  1. Создать заглушку для метода теста — то есть сигнатуру метода.

  2. Пройти один типичный тестовый случай.

  3. Протестировать более широкий диапазон. Убедиться, что код корректно реагирует на широкий диапазон значений.

  4. Исключение при неверных действиях. Правильная обработка некорректных параметров.

  5. Покрытие кода. Убедиться, что модульные тесты выполнили не менее 80 % кода.

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

Создание первого модульного теста

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

Это первый модульный тест для библиотеки классов, которую он тестирует, так что он создает новый проект модульного теста. Для этого он открывает диалоговое окно Создать проект и выбирает Visual C#, Тест, а затем Проект модульного теста.

Модульный тест, выбранный в диалоговом окне "Новый проект"

В этом проекте теста содержится файл C#, в который он может записывать свой пример. На этом этапе Питер просто намеревается показать, как будет извлекаться один из создаваемых им методов.

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Fabrikam.Math.UnitTest
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        // Demonstrates how to call the method.
        public void SignatureTest()
        {
            // Create an instance:
            var math = new Fabrikam.Math.LocalMath();

            // Get a value to calculate:
            double input = 0.0;

            // Call the method:
            double actualResult = math.SquareRoot(input);

            // Use the result:
            Assert.AreEqual(0.0, actualResult);
        }
    }
}

Питер записывает этот пример в методе теста, потому что ему нужно, чтобы к тому времени, когда он запишет код, пример работал.

Создание проекта модульного теста, а также методов

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

Для этой процедуры используется среда модульного тестирования Visual Studio, но можно работать и с платформами других поставщиков. Обозреватель тестов эффективно работает с ними — нужно лишь установить соответствующий адаптер.

  • Если проекта теста еще нет, создайте его.

    • В диалоговом окне Создать проект выберите язык, например Visual Basic, Visual C# или Visual C++. Нажмите Тест, а затем Проект модульного теста.
  • Добавьте тесты в предложенный класс теста. Каждый модульный тест представляет собой один метод.

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

            [TestMethod]
            public void SignatureTest()
            {...}
    
        <TestMethod()>
        Public Sub SignatureTest()
        ...
        End Sub
    
  • Каждый метод теста должен вызвать метод класса Assert, чтобы указать, был ли этот тест пройден. Как правило, достаточно проверить, совпадают ли ожидаемые и фактические результаты операции.

    Assert.AreEqual(expectedResult, actualResult);
    
    Assert.AreEqual(expectedResult, actualResult)
    
  • Методы тестов могут вызывать другие стандартные методы, которые не имеют атрибута TestMethod.

  • Тесты можно объединять более чем в один класс. Перед каждым классом указывается атрибут TestClass.

    [TestClass]
    public class UnitTest1
    { ... }
    
    <TestClass()>
    Public Class UnitTest1
    ...
    End Class
    

Дополнительные сведения о написании модульных тестов в C++ см. в разделе Написание модульных тестов для языка C/C++ с использованием платформы модульного тестирования Майкрософт для C++.

Создание заглушки для нового кода

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

Обозреватель решений с проектами тестов и классов

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

        public double SquareRoot(double p)
        {
            throw new NotImplementedException();
        }

Создание классов и методов из тестов

Если проекта, в который требуется добавить новый класс, еще нет, создайте его.

Создание класса

  1. Поместите курсор на пример класса, который нужно создать, LocalMath. В контекстном меню выберите пункт Создать код, Создать тип.

  2. В диалоговом окне Новый тип в поле Проект выберите проект библиотеки классов. В нашем примере проект имеет имя Fabrikam.Math.

Создание метода

  • Поместите курсор на вызов метода, например SquareRoot. В контекстном меню выберите пункт Создать код, Заглушка метода.

Выполнение первого теста

Питер выполняет сборку и запускает тест нажатием клавиш CTRL+R, T. В результатах теста отображается красный индикатор "Не пройдено", а сам тест отображается в списке Неудачные тесты.

Обозреватель модульных тестов с одним непройденным тестом

Питер вносит в код простое изменение.

       public double SquareRoot(double p)
        {
            return 0.0;
        }

Затем он вновь запускает тест, и на этот раз результат положительный.

Обозреватель модульных тестов с одним пройденным тестом

Запуск модульных тестов

Обозреватель тестов с кнопкой "Запустить все"

  • В меню Тест выберите Запуск, Все тесты.

    –или–

  • Если обозреватель тестов открыт, выберите команду Запустить все.

    - или -

  • Поместите курсор в файл кода теста и нажмите клавиши CTRL+R, T.

  • Если тест отображается в списке Неудачные тесты:

    Откройте тест, например дважды щелкните его имя.

    Будет показана точка, в которой в тесте возникла ошибка.

Чтобы просмотреть весь список тестов выберите Показать все. Для возвращения к сводке выберите представление HOME.

Чтобы просмотреть сведения о результатах теста, выделите тест в обозревателе тестов.

Чтобы перейти к коду теста, дважды щелкните тест в обозревателе тестов или выберите пункт Открыть тест в контекстном меню.

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

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

Согласование интерфейса

Питер звонит своей коллеге Джулии по Lync и открывает доступ к своему экрану. Она будет использовать его компонент. Питер показывает свой исходный пример.

Джулия соглашается, что пример подходит, но добавляет: "Этот тест пройдет множество функций".

В ответ Питер говорит: "Это самая первая версия теста, просто чтобы убедиться, что в функции правильно заданы параметры и имя. А сейчас можно написать тест, который проверит основное требование к этой функции".

Вместе они записывают следующий тест:

  
      [TestMethod]
        public void QuickNonZero()
        {
            // Create an instance to test:
            LocalMath math = new LocalMath();

            // Create a test input and expected value:
            var expectedResult = 4.0;
            var inputValue = expectedResult * expectedResult;

            // Run the method:
            var actualResult = math.SquareRoot(inputValue);

            // Validate the result:
            var allowableError = expectedResult/1e6;
            Assert.AreEqual(expectedResult, actualResult, allowableError,
                "{0} is not within {1} of {2}", actualResult, allowableError, expectedResult);
        }

Совет

Для этой функции Питер решил прибегнуть к методу "Сначала тест". Это значит, что первым делом он создаст тест для функции, а затем напишет код, который будет проходить этот тест.В других ситуациях, в которых такой подход непрактичен, он поступает наоборот: составляет тесты уже после написания кода.Однако, по его мнению, на каком бы этапе ни создавались модульные тесты — до написания кода или после него, — без них не обойтись, ведь они помогают в написании стабильных программ.

Красный, зеленый, рефакторинг…

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

Красный

Питер нажимает клавиши CTRL+R, T, чтобы запустить новый тест, созданный вместе с Джулией. Так он проверяет каждый записанный тест, чтобы убедиться, что без правильного кода этот тест будет выводить ошибку. Это правило он взял на вооружение после одного случая, когда он забыл вставить утверждения в пару тестов. Если на этом этапе тест дает отрицательный результат, Питер может быть уверен: когда он будет пройден, это действительно будет означать, что код удовлетворяет требованию.

Есть еще одна полезная привычка: устанавливать переключатель Выполнить тесты после сборки. Если он выбран, то при каждой сборке решения система будет прогонять тесты в фоновом режиме, и вам всегда будет доступен текущий отчет о состоянии тестирования кода. На первых порах Питер опасался, что это может замедлить работу Visual Studio, но позже он убедился, что такое случается крайне редко.

Обозреватель модульных тестов с одним непройденным тестом

Зеленый

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

    public class LocalMath
    {
        public double SquareRoot(double x)
        {
            double estimate = x;
            double previousEstimate = -x;
            while (System.Math.Abs(estimate - previousEstimate) > estimate / 1000)
            {
                previousEstimate = estimate;
                estimate = (estimate * estimate - x) / (2 * estimate);
            }
            return estimate;
        }
        

Затем он вновь выполняет тесты и получает положительный результат.

Обозреватель модульных тестов с двумя пройденными тестами

Рефакторинг

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

public class LocalMath
    {
        public double SquareRoot(double x)
        {
            double estimate = x;
            double previousEstimate = -x;
            while (System.Math.Abs(estimate - previousEstimate) > estimate / 1000)
            {
                previousEstimate = estimate; 
                estimate = (estimate + x / estimate) / 2;
                //was: estimate = (estimate * estimate - x) / (2 * estimate);
            }
            return estimate;
        }

Затем он вновь проверяет, что код проходит тест.

Обозреватель модульных тестов с двумя пройденными тестами

Совет

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

  • Рефакторинг — это такие изменения кода, при которых тесты остаются теми же самыми, поскольку новая функциональность не добавляется.

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

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

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

Выполняйте тесты после каждого изменения.

…и все с начала

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

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

        [TestMethod]
        public void SqRtValueRange()
        {
            LocalMath math = new LocalMath();
            for (double expectedResult = 1e-8;
                expectedResult < 1e+8;
                expectedResult = expectedResult * 3.2)
            {
                VerifyOneRootValue(math, expectedResult);
            }
        }
        private void VerifyOneRootValue(LocalMath math, double expectedResult)
        {
            double input = expectedResult * expectedResult;
            double actualResult = math.SquareRoot(input);
            Assert.AreEqual(expectedResult, actualResult, expectedResult / 1e6);
        }

Его код проходит этот тест уже при первом выполнении.

Обозреватель модульных тестов с тремя пройденными тестами

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

Совет

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

Исключения

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

[TestMethod]
        public void RootTestNegativeInput()
        {
            LocalMath math = new LocalMath();
            try
            {
                math.SquareRoot(-10.0);
            }
            catch (ArgumentOutOfRangeException)
            {
                return;
            }
            catch
            {
                Assert.Fail("Wrong exception on negative input");
                return;
            }
            Assert.Fail("No exception on negative input");
        }

В этом тесте код помещен в цикл. В этом случае в обозревателе тестов нужно нажать кнопку Отмена — тогда код будет остановлен в течение 10 секунд.

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

        [TestMethod, Timeout(1000)]
        public void RootTestNegativeInput()
        {...

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

Далее Питер обновляет код, чтобы вызвать исключение.

       public double SquareRoot(double x)
        {
            if (x <= 0.0) 
            {
                throw new ArgumentOutOfRangeException();
            }

Регрессия

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

Не пройден ранее пройденный модульный тест

Питер находит и исправляет ошибку.

      public double SquareRoot(double x)
        {
            if (x < 0.0)  // not <=
            {
                throw new ArgumentOutOfRangeException();
            }

Теперь все тесты проходят успешно.

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

Совет

Внося любые изменения в код, проверяйте, все ли тесты он проходит.

Покрытие кода

Время от времени в ходе работы (а также перед тем как вернуть код) Питер просматривает отчет о покрытии кода. В нем отображается, какая часть кода была выполнена тестами.

Команда Питера стремится к тому, чтобы покрытие кода составляло не менее 80 %. К автоматически создаваемому коду это требование смягчается, поскольку для него сложно добиться высокого коэффициента покрытия.

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

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

Результат покрытия кода и кнопка показа цвета

Коэффициент покрытия кода, который написал Питер, составляет 86 %. Развернув общие показатели по отчету, он видит, что разрабатываемый им код был протестирован на 100 %. Это превосходно, поскольку значение имеет именно объем кода, прошедшего тестирование. Непокрытые фрагменты относятся к самим тестам. Нажимая и отпуская кнопку Цвета отображения покрытия кода, Питер видит, какая часть кода не выполнялась. Он видит, что эти участки не имеют значения, поскольку они относятся к тестам и будут использоваться только при обнаружении ошибок.

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

Ожидаемый результат

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

  • Все доступные модульные тесты должны иметь состояние "Пройдено".

    Если в проекте очень большое количество модульных тестов, то разработчикам нет смысла дожидаться, пока все они будут выполнены. В таких случаях в проекте действует служба условного возврата, которая выполняет все автоматизированные тесты для каждого возвращаемого набора отложенных изменений, и лишь после этого объединяет его с деревом исходного кода. Если тест не пройден, возврат отклоняется. Благодаря этому разработчику приходится выполнять на своей машине лишь некоторые тесты, а затем он может переходить к дальнейшей работе, не опасаясь, что его код отрицательно скажется на сборке. Дополнительные сведения см. в разделе Использование процесса сборки с условным возвратом для проверки изменений.

  • Покрытие кода должно соответствовать принятому в команде стандарту. Стандартное требование к этому параметру в различных проектах — 75 %.

  • Модульные тесты должны имитировать все необходимые аспекты поведения, включая обычные и исключительные входные данные.

  • Код должен быть понятным и легко расширяемым.

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

Принципы разработки кода с модульными тестами

При разработке кода Питер руководствуется следующими принципами.

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

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

  • Тесты должны покрывать не менее 75 % кода. Время от времени в ходе работы, а также перед возвратом исходного кода просматривайте отчет о покрытии кода.

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

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

Возврат изменений

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

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

Когда сборка завершена, Питер получает уведомление. В окне результатов сборки он видит, что процесс завершен успешно и все тесты пройдены.

Возврат изменений

Возврат ожидающих изменений

  1. В строке меню выберите Вид, Team Explorer.

  2. В Team Explorer нажмите кнопку Домой, а затем Моя работа.

  3. На странице Моя работа нажмите кнопку Вернуть.

  4. Просмотрите содержимое страницы Ожидающие изменения, чтобы убедиться, что:

    • все необходимые изменения перечислены в разделе Включенные изменения;

    • все необходимые рабочие элементы перечислены в разделе Связанные рабочие элементы.

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

  6. Выберите Вернуть.

Непрерывная интеграция кода

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

Peter получает уведомление об успешном построении CI

Результаты построения CI

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

Далее. Приостановка работы, исправление ошибок и выполнение проверки кода