Практические примеры тестирования кода C # - PullRequest
15 голосов
/ 05 марта 2009

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

public class Hole : INotifyPropertyChanged
{
    #region Field Definitions
    private double _AbsX;
    private double _AbsY;
    private double _CanvasX { get; set; }
    private double _CanvasY { get; set; }
    private bool _Visible;
    private double _HoleDia = 20;
    private HoleTypes _HoleType;
    private int _HoleNumber;
    private double _StrokeThickness = 1;
    private Brush _StrokeColor = new SolidColorBrush(Colors.Black);
    private HolePattern _ParentPattern;
    #endregion

    public enum HoleTypes { Drilled, Tapped, CounterBored, CounterSunk };
    public Ellipse HoleEntity = new Ellipse();
    public Ellipse HoleDecorator = new Ellipse();
    public TextBlock HoleLabel = new TextBlock();

    private static DoubleCollection HiddenLinePattern = 
               new DoubleCollection(new double[] { 5, 5 });

    public int HoleNumber
    {
        get
         {
            return _HoleNumber;
         }
        set
        {
            _HoleNumber = value;
            HoleLabel.Text = value.ToString();
            NotifyPropertyChanged("HoleNumber");
        }
    }
    public double HoleLabelX { get; set; }
    public double HoleLabelY { get; set; }
    public string AbsXDisplay { get; set; }
    public string AbsYDisplay { get; set; }

    public event PropertyChangedEventHandler PropertyChanged;
    //public event MouseEventHandler MouseActivity;

    // Constructor
    public Hole()
    {
        //_HoleDia = 20.0;
        _Visible = true;
        //this.ParentPattern = WhoIsTheParent;
        HoleEntity.Tag = this;
        HoleEntity.Width = _HoleDia;
        HoleEntity.Height = _HoleDia;

        HoleDecorator.Tag = this;
        HoleDecorator.Width = 0;
        HoleDecorator.Height = 0;


        //HoleLabel.Text = x.ToString();
        HoleLabel.TextAlignment = TextAlignment.Center;
        HoleLabel.Foreground = new SolidColorBrush(Colors.White);
        HoleLabel.FontSize = 12;

        this.StrokeThickness = _StrokeThickness;
        this.StrokeColor = _StrokeColor;
        //HoleEntity.Stroke = Brushes.Black;
        //HoleDecorator.Stroke = HoleEntity.Stroke;
        //HoleDecorator.StrokeThickness = HoleEntity.StrokeThickness;
        //HiddenLinePattern=DoubleCollection(new double[]{5, 5});
    }

    public void NotifyPropertyChanged(String info)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, 
                       new PropertyChangedEventArgs(info));
        }
    }

    #region Properties
    public HolePattern ParentPattern
    {
        get
        {
            return _ParentPattern;
        }
        set
        {
            _ParentPattern = value;
        }
    }

    public bool Visible
    {
        get { return _Visible; }
        set
        {
            _Visible = value;
            HoleEntity.Visibility = value ? 
             Visibility.Visible : 
             Visibility.Collapsed;
            HoleDecorator.Visibility = HoleEntity.Visibility;
            SetCoordDisplayValues();
            NotifyPropertyChanged("Visible");
        }
    }

    public double AbsX
    {
        get { return _AbsX; }
        set
        {
            _AbsX = value;
            SetCoordDisplayValues();
            NotifyPropertyChanged("AbsX");
        }
    }

    public double AbsY
    {
        get { return _AbsY; }
        set
        {
            _AbsY = value;
            SetCoordDisplayValues();
            NotifyPropertyChanged("AbsY");
        }
    }

    private void SetCoordDisplayValues()
    {
        AbsXDisplay = HoleEntity.Visibility == 
        Visibility.Visible ? String.Format("{0:f4}", _AbsX) : "";
        AbsYDisplay = HoleEntity.Visibility == 
        Visibility.Visible ? String.Format("{0:f4}", _AbsY) : "";
        NotifyPropertyChanged("AbsXDisplay");
        NotifyPropertyChanged("AbsYDisplay");
    }

    public double CanvasX
    {
        get { return _CanvasX; }
        set
        {
            if (value == _CanvasX) { return; }
            _CanvasX = value;
            UpdateEntities();
            NotifyPropertyChanged("CanvasX");
        }
    }

    public double CanvasY
    {
        get { return _CanvasY; }
        set
        {
            if (value == _CanvasY) { return; }
            _CanvasY = value;
            UpdateEntities();
            NotifyPropertyChanged("CanvasY");
        }
    }

    public HoleTypes HoleType
    {
        get { return _HoleType; }
        set
        {
            if (value != _HoleType)
            {
                _HoleType = value;
                UpdateHoleType();
                NotifyPropertyChanged("HoleType");
            }
        }
    }

    public double HoleDia
    {
        get { return _HoleDia; }
        set
        {
            if (value != _HoleDia)
            {
                _HoleDia = value;
                HoleEntity.Width = value;
                HoleEntity.Height = value;
                UpdateHoleType(); 
                NotifyPropertyChanged("HoleDia");
            }
        }
    }

    public double StrokeThickness
    {
        get { return _StrokeThickness; }
        //Setting this StrokeThickness will also set Decorator
        set
        {
            _StrokeThickness = value;
            this.HoleEntity.StrokeThickness = value;
            this.HoleDecorator.StrokeThickness = value;
            NotifyPropertyChanged("StrokeThickness");
        }
    }

    public Brush StrokeColor
    {
        get { return _StrokeColor; }
        //Setting this StrokeThickness will also set Decorator
        set
        {
            _StrokeColor = value;
            this.HoleEntity.Stroke = value;
            this.HoleDecorator.Stroke = value;
            NotifyPropertyChanged("StrokeColor");
        }
    }

    #endregion

    #region Methods

    private void UpdateEntities()
    {
        //-- Update Margins for graph positioning
        HoleEntity.Margin = new Thickness
        (CanvasX - HoleDia / 2, CanvasY - HoleDia / 2, 0, 0);
        HoleDecorator.Margin = new Thickness
        (CanvasX - HoleDecorator.Width / 2, 
         CanvasY - HoleDecorator.Width / 2, 0, 0);
        HoleLabel.Margin = new Thickness
        ((CanvasX * 1.0) - HoleLabel.FontSize * .3, 
         (CanvasY * 1.0) - HoleLabel.FontSize * .6, 0, 0);
    }

    private void UpdateHoleType()
    {
        switch (this.HoleType)
        {
            case HoleTypes.Drilled: //Drilled only
                HoleDecorator.Visibility = Visibility.Collapsed;
                break;
            case HoleTypes.Tapped: // Drilled & Tapped
                HoleDecorator.Visibility = (this.Visible == true) ? 
                Visibility.Visible : Visibility.Collapsed;
                HoleDecorator.Width = HoleEntity.Width * 1.2;
                HoleDecorator.Height = HoleDecorator.Width;
                HoleDecorator.StrokeDashArray = 
                LinePatterns.HiddenLinePattern(1);
                break;
            case HoleTypes.CounterBored: // Drilled & CounterBored
                HoleDecorator.Visibility = (this.Visible == true) ? 
                Visibility.Visible : Visibility.Collapsed;
                HoleDecorator.Width = HoleEntity.Width * 1.5;
                HoleDecorator.Height = HoleDecorator.Width;
                HoleDecorator.StrokeDashArray = null;
                break;
            case HoleTypes.CounterSunk: // Drilled & CounterSunk
                HoleDecorator.Visibility = (this.Visible == true) ? 
                Visibility.Visible : Visibility.Collapsed;
                HoleDecorator.Width = HoleEntity.Width * 1.8;
                HoleDecorator.Height = HoleDecorator.Width;
                HoleDecorator.StrokeDashArray = null;
                break;
        }
        UpdateEntities();
    }

    #endregion

}

Ответы [ 13 ]

6 голосов
/ 05 марта 2009

Пример модульного теста:

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

Обычно это делается с помощью инфраструктуры тестирования, такой как NUnit .

(Довольно забавно, вы заметите, что свойство ParentPattern не вызывает событие.)

4 голосов
/ 05 марта 2009

Я расскажу вам великую тайну испытаний.

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

if( 1 + 1 == 2 ) {
    print "ok - 1 plus 1 equals 2\n";
}
else {
    print "not ok\n";
}

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

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

Я выразил это в таких упрощенных терминах, потому что люди, которые плохо знакомы с тестированием, часто поражены. Они думают, что есть какая-то магия. Некоторые специальные рамки, которые они должны использовать. Они часто даже забывают, что тесты все еще являются программами, и внезапно не могут подумать об использовании цикла или записи подпрограммы. Можно многому еще научиться, но, надеюсь, это даст вам ядро, вокруг которого можно будет выяснить, что это за «тест».

4 голосов
/ 05 марта 2009

Вы не можете надлежащим образом протестировать этот код, если также не указана спецификация. «Тестирование» обычно означает, что программное обеспечение работает как задумано.

РЕДАКТИРОВАТЬ: Это на самом деле не "отписаться" ответ. Ранее я работал тестировщиком и могу вам сказать, что почти все написанные мной тестовые примеры были получены непосредственно из спецификации программного обеспечения.

2 голосов
/ 05 марта 2009

В модульном тестировании вы просто проверяете свои "видимые" методы / свойства, а не частные.

Так, например, в своем коде вы можете добавить следующий тест:

hole.Visible = false;

Debug.Assert( "".Equals( hole.AbsXDisplay ) );
Debug.Assert( "".Equals( hole.AbsYDisplay ) );

Вы можете подумать: "Ну, это очевидно !" но через несколько недель вы можете забыть об этом. И если какая-то часть вашего кода зависит от значения AbsXDisplay (который является публичным атрибутом) и по какой-то причине после того, как вы установите для свойства значение false, оно больше не "", а "пусто" или " NotSet ", тогда этот тест не пройден, и вы будете немедленно уведомлены.

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

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

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

Надеюсь, это поможет вам понять, что представляет собой тест.

Как и предполагали другие, ищите рамки тестирования.

2 голосов
/ 05 марта 2009

Тестирование - это не просто инженерия, это искусство. То, что требует от вас читать. Я не уверен, что мы сможем научить вас через этот единственный вопрос всему, что вы хотите / нуждаетесь / должны / должны / должны знать. Для начала вот несколько вещей, которые вы можете проверить.

  • Единица (интерфейсы работают как положено)
  • Интеграция (компоненты ведут себя между собой)
  • Юзабилити (клиенты довольны)
  • Функционально (функция завершена)

Определите набор критериев для каждого (метрики) и начните тестирование.

1 голос
/ 06 марта 2009

Тесты помогут, если вам нужно внести изменения.

Согласно Перьям (Feathers, Эффективная работа с устаревшим кодом , стр. 3) есть четыре причины для изменений:

  • Добавление функции
  • Исправление ошибки
  • Улучшение дизайна
  • Оптимизация использования ресурсов

Когда есть необходимость в переменах, вы хотите быть уверены, что ничего не сломаете. Если быть более точным: вы не хотите нарушать какое-либо поведение (Хант, Томас, Прагматическое модульное тестирование в C # с NUnit , стр. 31).

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

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

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

Как получить положительный опыт модульного тестирования? Будьте открыты для этого и учитесь.

Я бы порекомендовал вам Эффективно работать с устаревшим кодом для существующего кода (как тот фрагмент кода, который вы дали выше). Чтобы легко начать работу с модульным тестированием, попробуйте Pragmatic Unit Testing в C # с NUnit . Реальным открытием для меня было xUnit Test Patterns: рефакторинг тестового кода .

Удачи в вашем путешествии!

1 голос
/ 06 марта 2009

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

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

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

Только private void UpdateHoleType(){...} содержит любую логику, которая кажется визуально ориентированной логикой, которую всегда сложнее всего проверить. Написание тестов очень просто. Ниже приведен пример для типа просверленного отверстия.

[Test]
public void testDrilledHole()
{
  Hole hole = new Hole();
  hole.HoleType = HoleTypes.Drilled;
  Assert.AreEqual(Visibility.Collapsed, hole.HoleDecorator.Visibility);
}

Если вы посмотрите на это, вы почти не посчитаете, что оно того стоит. Тест тривиален и очевиден. Атрибут [Test] объявляет метод тестом, а метод Assert.AreEquals() вызывает исключение, если предоставленные значения не равны. Фактическая конструкция может варьироваться в зависимости от используемой среды тестирования, но все они одинаково просты.

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

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

1 голос
/ 05 марта 2009

Вот пример. Помните, что в вашем примере кода отсутствуют определения ряда зависимостей:

[TestFixture()]
public class TestHole 
{

    private Hole _unitUnderTest;

    [SetUp()]
    public void SetUp() 
    {
        _unitUnderTest = new Hole();
    }

    [TearDown()]
    public void TearDown() 
    {
        _unitUnderTest = null;
    }

    [Test]
    public void TestConstructorHole()
    {
        Hole testHole = new Hole();
        Assert.IsNotNull(testHole, "Constructor of type, Hole failed to create instance.");
    }

    [Test]
    public void TestNotifyPropertyChanged()
    {
        string info = null;
        _unitUnderTest.NotifyPropertyChanged(info);
    }
}

Вы можете видеть, что он проверяет, что конструктор создает допустимый объект (обычно нет необходимости с полным испытательным приспособлением на месте, конструирование обычно хорошо выполняется), а также тестирует единственный открытый метод в классе. В этом случае вам потребуется делегат обработчика событий и Assert для проверки содержимого информационного параметра.

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

1 голос
/ 05 марта 2009

Как бы там ни было, кажется, что большую часть этого класса не нужно проверять (кроме ответа Горда ), если класс был написан по-другому. Например, вы смешиваете информацию о модели (тип отверстия и т. Д.) С информацией о представлении (толщина). Кроме того, я думаю, что вы упускаете точку WPF и привязку данных / триггеры. UpdateHoleType () Должен быть выражен в файле .xaml как набор DataTriggers, так же, как UpdateEntities () и большинство других ваших свойств.

0 голосов
/ 06 марта 2009

С точки зрения запуска события Notify вы должны обязательно убедиться, что ваш класс работает в соответствии со спецификацией, а именно:

  • Родитель никогда не сработает независимо от установленного значения
  • StrokeColour и StrokeThickness всегда запускают событие, даже если установлено одинаковое значение
  • CanvasX / Y, HoleType / Dia срабатывает, только если установлено значение, отличное от предыдущего

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...