Как я выработал привычку писать нестандартные тесты


Введение

С тех пор как я начал свою карьеру в области разработки программного обеспечения, мне пришлось освоить огромное количество навыков, помимо «написания кода».
На своей работе я в основном использую C#, и объектно-ориентированное кодирование было для меня иностранным языком, поскольку я занимался только численными расчетами, используя другие языки программирования. В то время мне не нужно было задумываться о классах и их взаимосвязи с другими классами.

Трудности в работе

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

Знакомство с нетестированием

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

Один из самых простых примеров, представленных в Интернете

Самым простым юниттестом может быть метод, выполняющий суммирование.

public class Arithmetic
{
    public int Summation(int a,int b)
    {
        return a + b;
    }    
}
Вход в полноэкранный режим Выход из полноэкранного режима

Мы можем написать unittest для этого кода в NUnit следующим образом.

public class Tests
{
    private Arithmetic arithmetic;
    [SetUp]
    public void Setup()
    {
        arithmetic = new Arithmetic();
    }

    [Test]
    public void SummationTest()
    {
        //Arrange
        int a = 1;
        int b = 1;
        //Act
        int result = arithmetic.Summation(a,b);
        //Assert
        Assert.AreEqual(2,result);
    }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Приведенный выше код прошел тест. Очевидно, что мы можем писать тесты, если производственный код достаточно прост, как в примере с суммированием выше. Мы проверили, что 1+1 возвращает 2.

Другой пример

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

public class House
{   
    public int Location { get; set; }
}
Вход в полноэкранный режим Выход из полноэкранного режима
public class CoupledCat
{
    private House house;
    public int Location { get; set; }

    public CoupledCat()
    {
        house = new House();
        house.Location = 1;
    }

    public int RelativeDistance()
    {
        return Math.Abs(house.Location - this.Location);
    }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Мы можем запустить код.

static void Main(string[] args)
    {

        //Prepare a cat with his house information
        CoupledCat cat = new CoupledCat();
        cat.Location = 3;

        //get distance
        int rel = cat.RelativeDistance();
        Console.WriteLine("you need to walk "+rel+" meter(s)");        
    }
Войти в полноэкранный режим Выход из полноэкранного режима

Тест для класса Coupled Cat

Мы можем написать тест для класса CoupledCat.

public class CoupledCatTest
{
    private CoupledCat cat;
    [SetUp]
    public void Setup()
    {
        cat = new CoupledCat();
    }

    [Test]
    public void DistanceTest()
    {
        //Arrange
        cat.Location = 3;
        //Act
        int result = cat.RelativeDistance();
        //Assert
        Assert.AreEqual(2,result);
    }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Мы можем написать тест для этого класса, однако это неудобно, поскольку в классе CoupledCat местоположение дома фиксировано, а мы хотели бы его переместить.

Инъекция зависимостей

Я хотел бы иметь некоторую свободу в отношении расположения дома. Чтобы устранить эту зависимость, мы можем применить инъекцию зависимостей. Существует три способа ее осуществления: 1.инъекция свойств, 2.инъекция конструкторов и 3.инъекция методов.

1.Инъекция свойств

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

public class Cat
{
    public House House { get; set; }
    public int Location { get; set; }
    public int RelativeDistance()
    {
        return Math.Abs(House.Location - this.Location);
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима
public class House
{   
    public int Location { get; set; }
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

class Program
{
    static void Main(string[] args)
    {
        //Prepare a house
        House house = new House();
        house.Location = 1;

        //Prepare a cat with his house information
        Cat cat = new Cat();
        cat.House = house;
        cat.Location = 3;

        //get distance
        int rel = cat.RelativeDistance();
        Console.WriteLine("you need to walk "+rel+" meter(s)");
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

2.Инъекция конструктора

Мы можем изменить место инъекции со свойства на конструктор. Экземпляр дома должен быть подготовлен до создания клана кошки.

public class Cat
{
    private House house;
    public int Location { get; set; }

    public Cat(House house)
    {
        this.house = house;
    }
    public int RelativeDistance()
    {
        return Math.Abs(house.Location - this.Location);
    }
}
Войти в полноэкранный режим Выход из полноэкранного режима

3.Инъекция метода

Поскольку класс house нужен нам только в методе RelativeDistance, мы можем инжектировать его экземпляр в метод.

public class Cat
{
    public int Location { get; set; }

    public int RelativeDistance(House house)
    {
        return Math.Abs(house.Location - this.Location);
    }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Написание теста для примера с домом-кошкой

Я напишу тест для инъекции свойств на основе класса cat.

public class CatHouseTest
{
    private House house;
    private Cat cat;
    [SetUp]
    public void Setup()
    {
        house = new House();
        cat = new Cat();
        cat.House = house;
    }

    [Test]
    public void DistanceTest()
    {
        //Arrange
        house.Location = 1;
        cat.Location = 3;
        //Act
        int result = cat.RelativeDistance();
        //Assert
        Assert.AreEqual(2,result);        
    }  
}
Вход в полноэкранный режим Выход из полноэкранного режима

Добавление гибкости в класс кошки

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

Внедрение интерфейса

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

public interface IBuilding
{
    int Location { get; set; }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Класс house может реализовать IBuilding, поскольку у дома есть свойство location.

public class House:IBuilding
{
    public int Location { get; set; }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Давайте вернемся к классу кошки. Мы можем заменить свойство House в классе cat на интерфейс здания.

public class Cat
{
    public IBuilding Building { get; set; }
    public int Location { get; set; }
    public int RelativeDistance()
    {
        return Math.Abs(Building.Location - this.Location);
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Мы можем запустить код, и между предыдущим и новым кодом не будет большой разницы, кроме замены cat.House.

class Program
{
    static void Main(string[] args)
    {
        //Prepare a house
        House house = new House();
        house.Location = 1;

        //Prepare a cat with his house information
        Cat cat = new Cat();
        cat.Building = house;
        cat.Location = 3;

        //get distance
        int rel = cat.RelativeDistance();
        Console.WriteLine("you need to walk "+rel+" meter(s)");
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

public class FishMarket:IBuilding
{
    public int Location { get; set; }
}
Вход в полноэкранный режим Выход из полноэкранного режима
class Program
{
    static void Main(string[] args)
    {
        //Prepare a house
        House house = new House();
        house.Location = 1;

        //Prepare a fishmarket
        FishMarket fishMarket = new FishMarket();
        fishMarket.Location = 5;

        //Prepare a cat with his house information
        Cat cat = new Cat();
        cat.Location = 2;
        cat.Building = house;

        //get distance
        int relToHouse = cat.RelativeDistance();
        Console.WriteLine("you need to walk "+relToHouse+" meter(s)");

        //Set fishmarket
        cat.Building = fishMarket;

        //get distance
        int relToFishMarket = cat.RelativeDistance();
        Console.WriteLine("you need to walk "+relToFishMarket+" meter(s)");
    }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Написание нескольких тестов

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

public class CatHouseTest
{
    private House house;
    private FishMarket fishMarket;
    private Cat cat;
    [SetUp]
    public void Setup()
    {
        house = new House();
        fishMarket = new FishMarket();
        cat = new Cat();       
    }

    [Test]
    public void DistanceToHouseTest()
    {
        //Arrange
        house.Location = 1;
        cat.Building = house;
        cat.Location = 3;
        //Act
        int result = cat.RelativeDistance();
        //Assert
        Assert.AreEqual(2,result);
    }

    [TestCase(0,1,1)]
    [TestCase(8,2,10)]
    [TestCase(4,-1,3)]
    public void DistanceToFishMarketTest(int expected,int catLocation,int fishMarketLocation)
    {
        //Arrange
        fishMarket.Location = fishMarketLocation;
        cat.Location = catLocation;
        cat.Building = fishMarket;
        //Act
        int result = cat.RelativeDistance();
        //Assert
        Assert.AreEqual(expected,result);
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Результат — все зеленые, так что мы очень довольны результатом!

Использование внешнего ресурса

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

public class Cat
{
    public IBuilding Building { get; set; }
    public int Distance { get; set; }

    public int LoadLocationFromText()
    {
        string text = System.IO.File.ReadAllText(path+location.txt);
        return Convert.ToInt32(text);
    }
    public int RelativeDistance()
    {
        int location = LoadLocationFromText();
        return Math.Abs(Building.Location - location);
    }

}
Вход в полноэкранный режим Выйти из полноэкранного режима

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

Представьте класс testclass

мы заинтересованы в тестировании метода RelativeDistance и метода LoadLocationFromText в зависимости от текстового файла на моем компьютере. Это означает, что если тест будет выполняться на чужом компьютере, он не будет работать, если не предоставить текстовый файл и путь к нему. Лучше, если мы сможем запустить тест без подготовки внешнего ресурса.
Если нам разрешено вносить некоторые изменения в методы класса, мы можем переписать метод LoadLocationFromText, используя ключевое слово virtual. Если метод является виртуальным, то мы можем переопределить его в тесте. Это позволяет нам безопасно игнорировать внешнюю зависимость.

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

public class CatTest : Cat
{
    private int input;
    public CatTest(int input)
    {
        this.input = input;
    }
    public override int LoadLocationFromText()
    {
        return this.input;
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима
public class CatHouseTest
{
    private House house;
    private FishMarket fishMarket;
    private CatTest cat;
    [SetUp]
    public void Setup()
    {
        house = new House();
        fishMarket = new FishMarket();
    }

    [Test]
    public void DistanceToHouseTest()
    {
        //Arrange
        cat = new CatTest(3);
        house.Location = 1;
        cat.Building = house;
        //Act
        int result = cat.RelativeDistance();
        //Assert
        Assert.AreEqual(2,result);
    }

    [TestCase(0,1,1)]
    [TestCase(8,2,10)]
    [TestCase(4,-1,3)]
    public void DistanceToFishMarketTest(int expected,int catLocation,int fishMarketLocation)
    {
        //Arrange
        cat = new CatTest(catLocation);
        fishMarket.Location = fishMarketLocation;
        cat.Building = fishMarket;
        //Act
        int result = cat.RelativeDistance();
        //Assert
        Assert.AreEqual(expected,result);
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь он проходит все тесты.

Заключение

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

  1. Найдите самый маленький класс в проекте. Важно найти класс с наименьшим количеством зависимостей.
  2. Напишите один простой тест для этого класса.
  3. Если есть огромный класс, разбейте его на более мелкие классы.
  4. Напишите тесты для этих небольших классов.
  5. Найдите класс с внешними зависимостями, такими как загрузка файлов, подключение к базе данных и так далее.
  6. Если их можно изменить, сделайте метод виртуальным и переопределите его, создав класс для тестирования.
  7. Поскольку проект начал содержать некоторые тесты, должно быть легче модифицировать код, не слишком беспокоясь о появлении ошибок.
  8. На данный момент код должен выглядеть намного лучше, чем раньше, и я не боюсь писать тесты для своего кода. Следующим шагом для меня будет правильная подготовка макета в моем тесте.

Оцените статью
devanswers.ru
Добавить комментарий