- Введение
- Трудности в работе
- Знакомство с нетестированием
- Один из самых простых примеров, представленных в Интернете
- Другой пример
- Тест для класса Coupled Cat
- Инъекция зависимостей
- 1.Инъекция свойств
- 2.Инъекция конструктора
- 3.Инъекция метода
- Написание теста для примера с домом-кошкой
- Добавление гибкости в класс кошки
- Внедрение интерфейса
- Написание нескольких тестов
- Использование внешнего ресурса
- Представьте класс testclass
- Заключение
Введение
С тех пор как я начал свою карьеру в области разработки программного обеспечения, мне пришлось освоить огромное количество навыков, помимо «написания кода».
На своей работе я в основном использую 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);
}
}
Теперь он проходит все тесты.
Заключение
Главной целью для меня было выработать привычку к нетестированию.
Однако самым большим препятствием было понимание того, как писать тесты. Я начал писать тесты и проводить рефакторинг следующим образом.
- Найдите самый маленький класс в проекте. Важно найти класс с наименьшим количеством зависимостей.
- Напишите один простой тест для этого класса.
- Если есть огромный класс, разбейте его на более мелкие классы.
- Напишите тесты для этих небольших классов.
- Найдите класс с внешними зависимостями, такими как загрузка файлов, подключение к базе данных и так далее.
- Если их можно изменить, сделайте метод виртуальным и переопределите его, создав класс для тестирования.
- Поскольку проект начал содержать некоторые тесты, должно быть легче модифицировать код, не слишком беспокоясь о появлении ошибок.
- На данный момент код должен выглядеть намного лучше, чем раньше, и я не боюсь писать тесты для своего кода. Следующим шагом для меня будет правильная подготовка макета в моем тесте.