Евгений Колесников 2 роки тому
батько
коміт
a10b7457fe

+ 3 - 0
.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+    "dotnet.preferCSharpExtension": true
+}

+ 105 - 85
articles/5_3_1_10_unit_test.md

@@ -1,10 +1,6 @@
-<table style="width: 100%;"><tr><td style="width: 40%;">
-<a href="../articles/null.md">
-</a></td><td style="width: 20%;">
-<a href="../readme.md">Содержание
-</a></td><td style="width: 40%;">
-<a href="../articles/fake_unit_test.md">Fake data. Тестирование методов получающих внешние данные из удалённых источников.
-</a></td><tr></table>
+Предыдущая лекция | &nbsp; | Следующая лекция
+:----------------:|:----------:|:----------------:
+[Создание библиотеки классов](./5_3_1_9_classlib.md) | [Содержание](../readme.md#мдк-0503-тестирование-информационных-систем) | [Fake data. Тестирование методов получающих внешние данные из удалённых источников](./fake_unit_test.md)
 
 # Создание UNIT-тестов
 
@@ -24,7 +20,7 @@
 
 Принцип именования методов тестирующего класса:
 
-`[Тестирующийся метод]_[Сценарий]_[Ожидаемое поведение]`
+`[Тестируемый метод]_[Сценарий]_[Ожидаемое поведение]`
 
 Примеры:
 
@@ -36,18 +32,20 @@
 Любой тест проходит три стадии:
 
 1. **Arrange** - подготовка тестовых данных
+
     ```cs
     int x = 10;
     int y = 20;
     int expected = 30;
     ```
 
-2. **Act** - выполнение основного действия тестируемым классом
+1. **Act** - выполнение основного действия тестируемым классом
+
     ```cs
     int actual = Calc.Add(x, y)
     ```
 
-3. **Assert** - проверка результата
+1. **Assert** - проверка результата
     ```cs
     Assert.AreEqual(exprected, actusl);
     ```
@@ -99,108 +97,126 @@
 
 ## Практика
 
-В контекстном меню **РЕШЕНИЯ** выбираете *Добавить -> Создать проект*
-
-При создании проекта можете воспользоваться фильтрами "языки", "платформа" и "типы проектов". В итоге надо выбрать "Проект модульного теста (.NET Framework)"
-
-![](../img/task086.png)
-
-Правила формирования имени тестового проекта были выше, у меня получилось **orm3.Tests**. Расположение не трогаем, по-умолчанию тестовый проект сохранится в том же решении (рядом с основным проектом)
+В решение, где у вас создана библиотека классов, добавьте проект "Unit Test Project":
 
-### Связь с основным проектом
-
-Дальше нужно добавить связь с основным проектом:
-
-В тестовом проекте находим пункт "зависимости" (теперь он называется "ссылки") и в контекстном меню "добавить ссылку на проект"
+![](../img/rider023.png)
 
-![](../img/task087.png)
+* Правила формирования имени тестового проекта были выше, у меня получилось **CompanyCoreLib.Tests**. 
+* Расположение не трогаем, по-умолчанию тестовый проект сохранится в том же решении (рядом с основным проектом). 
+* Тип тестов: **MSTest**
 
-Выбрать основной проект (их может быть несколько), у меня он называется **orm3**.
+Получится такая "рыба":
 
-![](../img/task088.png)
+```cs
+namespace CompanyCoreLib.Tests;
 
-Проверить, добавлен ли проект в зависимости, можно раскрыв "зависимости"
+[TestClass]
+public class UnitTest1
+{
+    [TestMethod]
+    public void TestMethod1()
+    {
+    }
+}
+```
 
-![](../img/task089.png)
+### Связь с основным проектом
 
+Добавьте связь с основным проектом (как делали в прошлой лекции в консольном приложении).
 
 ### Написание тестов
 
-Для начала нужно выбрать в тестируемом проекте класс, который мы хотим протестировать. Т.к. у нас там нет каких-то простых классов, то нарисуем класс "Калькулятор" с методом "Деление" (желательно отдельным файлом, т.к. это может оцениваться отдельными баллами, но можно и в любом .cs файле проекта):
+Для начала нужно выбрать в тестируемом проекте (CompanyCoreLib) класс, который мы хотим протестировать - у нас там один класс, созданный именно для тестирования: **Analytics**.
 
-```cs
-public class Calc
-{
-    public float Div(float a, float b)
-    {
-        return a / b;
-    }
-}
-```
+Тестирующий класс переименуйте в **AnalyticsTests** (по соглашению к имени тестируемого класса добавляется суффикс *Tests*)
 
-Тестовый метод переобзываем в соответствии с соглашением: **Div_2div2_1expected**, т.е. мы делим "2" на "2" и ожидаем получить "1".
+Тестовый метод тоже переименуем в соответствии с соглашением. Но для начала мы должны определиться что будем тестировать. Повторим тот код, который реализовывали в консольном приложении, т.е. на входе у нас список уникальных дат, а на выходе отсортированный список: **PopularMonths_UniqueDates_OnlySort**.
 
 и реализуем этот метод:
 
 ```cs
 [TestMethod]
-public void Div_2div2_1expected()
+public void PopularMonths_UniqueDates_OnlySort()
 {
-    //arrange
-    int a = 2;
-    int b = 2;
-    float expected = 1;
-
-    //act
-    // тут мы должны создать экземпляр класса, чтобы протестировать его
-    Calc MyCalc = new Calc();
-    float actual = MyCalc.Div(a, b);
-
-    //assert
-    Assert.AreEqual(actual, expected);
+    // Arrange - подготовка исходных данных
+    var srcDates = new List<DateTime>()
+    {
+        new DateTime(2023,12,1,0,0,0),
+        new DateTime(2023,11,1,0,0,0),
+    };
+    
+    /**
+        * Act - действие
+        * тут мы должны создать экземпляр класса, чтобы протестировать его
+        */
+    var myAnalytics = new Analytics();
+    
+    // выполняем метод и получаем результат
+    var outDates = myAnalytics.PopularMonths(srcDates);
+    
+    /**
+    * подгатавливаем данные для 
+    * ожидаемого результата
+    * просто оригинальный список, 
+    * но с сортировкой по возрастанию
+    */
+    var expectedDates = new List<DateTime>()
+    {
+        new DateTime(2023,11,1,0,0,0),
+        new DateTime(2023,12,1,0,0,0)
+    };
+    
+    // Assert - сравниваем ожидаемый результат и данные полученные от тестируемого метода
+    CollectionAssert.AreEqual(expectedDates, outDates);
+
 }
 ```
 
-При ошибке сравнения методы **Assert** вызывают исключения, по которым система и определяет пройден тест или нет.
+При сравнении скалярных данных (числа, строки...) используется класс **Assert**. Но так как у нас не скалярный тип данных (**List**), то нужно использовать соответствующий класс - нам подходит **CollectionAssert**
+
+![](../img/rider024.png)
+
+При ошибке сравнения данных методы **Assert** вызывают исключения, по которым система и определяет пройден тест или нет.
 
 ## Запуск тестов
 
-1. Пересоберите тестовый проект, чтобы убедиться что нет ошибок: *Сборка -> Собрать решение*
+1. Пересоберите решение, чтобы убедиться что нет ошибок
 
-2. Откройте "обозреватель тестов": *Тест -> Обозреватель тестов* и выполните тест
+1. Запустите тест: *`Tests -> Run Unit Test`*
 
-    ![](../img/task090.png)
+    ![](../img/rider025.png)
 
 ## Оптимизация
 
-Считается хорошим тоном в тестовых методах делать только то, что непосредственно относится к тестированию. В нашем случае создание экземпляра класса *MyCalc* выбивается из общей картины и, к тому же, мы будем вынуждены это делать в каждом тестовом методе.
+Считается хорошим тоном в тестовых методах делать только то, что непосредственно относится к тестированию. В нашем случае создание экземпляра класса *myAnalytics* выбивается из общей картины и, к тому же, мы будем вынуждены это делать в каждом тестовом методе.
 
 Чтобы избежать повторений и очистить код добавим служебные методы инициализации и финализации:
 
 ```cs
-// сам экземпляр класса "Calc" мы объявляем как свойство тестового класса
-static Calc MyCalc = null;
+// сам экземпляр класса "Analytics" мы объявляем 
+// как свойство тестирующего класса
+private static Analytics myAnalytics = null;
 
 /* используем атрибут [ClassInitialize], 
 который скажет тестировщику, 
 что этот метод нужно выполнить ДО запуска тестов 
-(поэтому и этот метод и свойство Mycalc 
-должны быть объявлены статические)*/
+(поэтому и этот метод и свойство myAnalytics 
+должны быть объявлены статическими)*/
 [ClassInitialize]
 static public void Init(TestContext tc)
 {
-    MyCalc = new Calc();
+    myAnalytics = new Analytics();
 }
 
 // и аналогично для завершения
 [ClassCleanup]
 static public void Done()
 {
-    MyCalc = null;
+    myAnalytics = null;
 }
 ```
 
-А в методе *Div_2div2_1expected* создание экземпляра *MyCalc* убрать.
+А в методе *PopularMonths_UniqueDates_OnlySort* создание экземпляра *myAnalytics* убрать.
 
 ## Добавление тестовых ситуаций (case)
 
@@ -210,22 +226,32 @@ static public void Done()
 
     ```cs
     [TestMethod]
-    public void Div_2div2_2notexpected()
+    public void PopularMonths_UniqueDates_NotEmpty()
     {
-        //arrange
-        int a = 2;
-        int b = 2;
-        float expected = 2;
-
-        //act
-        float actual = MyCalc.Div(a, b);
-
-        //assert
-        Assert.AreNotEqual(actual, expected);
+        // Arrange - подготовка исходных данных
+        var srcDates = new List<DateTime>()
+        {
+            new DateTime(2023,12,1,0,0,0),
+            new DateTime(2023,11,1,0,0,0),
+        };
+        
+        // Act - действие
+        var outDates = myAnalytics.PopularMonths(srcDates);
+        
+        /**
+         * Arrange - подгатавливаем данные для ожидаемого результата
+         * пустой список для проверки не ожиданного результата
+         */
+        var expectedDates = new List<DateTime>();
+        
+        CollectionAssert.AreNotEqual(expectedDates, outDates);
     }
     ```
 
 * используя атрибут *ExpectedException*, проверяем наличие исключения при делении на 0
+
+    > Наш метод исключений не вызывает, поэтому тут "сферический конь в вакууме" от другого метода
+
     ```cs
     [TestMethod]
     [ExpectedException(typeof(DivideByZeroException), "Деление на 0")]
@@ -244,7 +270,7 @@ static public void Done()
     }
     ```
 
-    То же самое можно сделать без использования атрибута, а просто завернув код в блок try..catch, и в секции try после вычисления вставить `Assert.Fail();` (т.е. если при делении на 0 вдруг попадем на этот код, то тест не пройден)
+    То же самое можно сделать без использования атрибута, а просто завернув код в блок `try..catch`, и в секции try после вычисления вставить `Assert.Fail();` (т.е. если при делении на 0 вдруг попадем на этот код, то тест не пройден)
 
 ## Ближе к телу
 
@@ -288,9 +314,7 @@ static public void Done()
     }
     ```
 
-    >Вообще вместо исключений можно использовать код ответа, т.е. если сохранение произошло успешно, то возвращать "0", иначе код ошибки (а можно и сразу текст ошибки).
-
-2. Реализуем тестирование
+1. Реализуем тестирование
 
     ```cs
     namespace UnitTestProject1
@@ -313,10 +337,6 @@ static public void Done()
 
     Тут используется атрибут **ExpectedException**. Он означает, что НОРМАЛЬНЫМ завершением этого теста будет исключение указанного типа.
 
-<table style="width: 100%;"><tr><td style="width: 40%;">
-<a href="../articles/null.md">
-</a></td><td style="width: 20%;">
-<a href="../readme.md">Содержание
-</a></td><td style="width: 40%;">
-<a href="../articles/fake_unit_test.md">Fake data. Тестирование методов получающих внешние данные из удалённых источников.
-</a></td><tr></table>
+Предыдущая лекция | &nbsp; | Следующая лекция
+:----------------:|:----------:|:----------------:
+[Создание библиотеки классов](./5_3_1_9_classlib.md) | [Содержание](../readme.md#мдк-0503-тестирование-информационных-систем) | [Fake data. Тестирование методов получающих внешние данные из удалённых источников](./fake_unit_test.md)

+ 104 - 79
articles/5_3_1_9_classlib.md

@@ -1,3 +1,7 @@
+Предыдущая лекция | &nbsp; | Следующая лекция
+:----------------:|:----------:|:----------------:
+[Реинжиниринг бизнес-процессов в информационных системах.](./5_3_1_8_reengeniring.md) | [Содержание](../readme.md#мдк-0503-тестирование-информационных-систем) | [Создание UNIT-тестов](./5_3_1_10_unit_test.md)
+
 # Разработка библиотеки классов
 
 ## Техническое задание
@@ -6,12 +10,9 @@
 
 ### Общие требования
 
-В связи со стремительным развитием нашей системы было решено вынести некоторый важный функционал за рамки основного проекта и сделать библиотеку классов, которую мы сможем подключать
-к любому нашему проекту в случае расширения. Данная библиотека будет подключаться к основному проекту и должна быть представлена в виде *.dll/.jar* файла или папки с файлом *.py*.
+В связи со стремительным развитием нашей системы было решено вынести некоторый важный функционал за рамки основного проекта и сделать библиотеку классов, которую мы сможем подключать к любому нашему проекту в случае расширения. Данная библиотека будет подключаться к основному проекту и должна быть представлена в виде `*.dll/.jar*` файла или папки с файлом `*.py*`.
 
-Чтобы система правильно интегрировалась вам необходимо обязательно следовать правилам именования библиотек, классов и методов в них. В случае ошибок в рамках именования ваша работа не
-может быть проверена и ваш результат не будет зачтен. Классы и методы должны содержать модификатор **public** (если это реализуемо в рамках платформы), чтобы внешние приложения могли
-получить к ним доступ.
+Чтобы система правильно интегрировалась вам необходимо обязательно следовать правилам именования библиотек, классов и методов в них. В случае ошибок в рамках именования ваша работа не может быть проверена и ваш результат не будет зачтен. Классы и методы должны содержать модификатор **public** (если это реализуемо в рамках платформы), чтобы внешние приложения могли получить к ним доступ.
 
 В качестве названия для библиотеки необходимо использовать: **CompanyCoreLib**. Вам необходимо загрузить исходный код проекта с библиотекой в отдельный репозиторий с названием, совпадающим с названием приложения.
 
@@ -21,9 +22,8 @@
 
 Реализуйте метод, который принимает в себя список объектов даты и времени по совершенным покупкам/заказам в рамках нашей компании, а возвращает список дат (без времени), отсортированный в порядке уменьшения частоты заказов. Это необходимо, чтобы наша компания могла прогнозировать наиболее высокий спрос на следующий год для обеспечения более качественного оказания услуг.
 
-Возвращаемые данные должны содержать только даты для первого числа каждого месяца и 00:00 минут. 
-Например, вам поступили следующие данные: 2019-12-12 14:43, 2019-12-01 15:05, 2019-11-04 09:01, а, значит, самый популярный месяц - декабрь. Вам необходимо вернуть следующие данные: 2019-12-01 00:00, 2019-11-01 00:00. В случае совпадения характеристик популярности сперва нужно вывести более
-ранние месяцы.
+Возвращаемые данные должны содержать только даты для первого числа каждого месяца и `00:00` минут. 
+Например, вам поступили следующие данные: `2019-12-12 14:43, 2019-12-01 15:05, 2019-11-04 09:01`, а, значит, самый популярный месяц - декабрь. Вам необходимо вернуть следующие данные: `2019-12-01 00:00, 2019-11-01 00:00`. В случае совпадения характеристик популярности сперва нужно вывести более ранние месяцы.
 
 Прогноз строится на основе предыдущего года. ~~Так что данные Вам будут выдаваться строго за предыдущий год.~~ *Данные будут произвольные, поэтому даты не удовлеторяющие этому условию нужно игнорировать.*
 
@@ -33,34 +33,32 @@
 
 &nbsp; | C#
 ---|---
-Библиотека классов | CompanyCoreLib.dll
-Название класса | Analytics
-Название метода | PopularMonths()
-Входящие обязательные параметры | List\<DateTime\> dates
-Возвращаемые параметры | List\<DateTime\>
+Библиотека классов | `CompanyCoreLib.dll`
+Название класса | `Analytics`
+Название метода | `PopularMonths()`
+Входящие обязательные параметры | `List<DateTime> dates`
+Возвращаемые параметры | `List<DateTime>`
 
 ## Реализация
 
 ### Создание библиотеки классов
 
-Создаем **новый** проект, установив нужные фильтры (*C#*, *Windows*, *Библиотека*) и выбрав проект для соответствующей платформы. **Мы всё делаем для .NET Core**.
+Создаем **новое решение**, выбрав шаблон `.NET / Class Library`.
 
-![создание проекта](../img/test_01.png)
+![создание проекта](../img/rider016.png)
 
-Не забываем указывать название проекта. Как вы тут напишете, так dll и будет называться.
+Не забываем указывать название проекта по требованиям из ТЗ. Как вы тут напишете, так dll и будет называться.
 
-![ввод названия](../img/test_02.png)
+В итоге IDE создаст нам "рыбу" с одним файлом:
 
-В итоге студия создаст нам "рыбу" с одним файлом:
+![](../img/rider017.png)
 
-```cs
-using System;
 
+```cs
 namespace CompanyCoreLib
+
+public class Class1
 {
-    public class Class1
-    {
-    }
 }
 ```
 
@@ -68,16 +66,17 @@ namespace CompanyCoreLib
 
 По ТЗ наш класс должен называться **Analytics** - в *обозревателе решений* переименуйте файл `Class1.cs` в `Analytics.cs`. Система спросит, переименовать ли все названия в проекте - соглашаемся. В итоге наш класс должен выглядеть так:
 
+![](../img/rider018.png)
+
 ```cs
 namespace CompanyCoreLib
+
+public class Analytics
 {
-    public class Analytics
-    {
-    }
 }
 ```
 
-Обратите внимание на модификатор **public** у класса - не во всех версиях *Visual Studio* он ставится автоматически. Добавьте, если его нет.
+Обратите внимание на наличие модификатора **public** у класса - не во всех версиях он ставится автоматически. Добавьте, если его нет.
 
 ### Создание метода
 
@@ -93,21 +92,18 @@ public List<DateTime> PopularMonths(List<DateTime> dates) {
 
 `<Модификатор доступа> <Тип результата, возвращаемого методом> НазваниеМетода(<Тип параметра 1> <НазваниеПараметра>[, <Тип параметра 2> <НазваниеПараметра2>])`
 
->В ТЗ явно указано, что название параметра **dates**, и хотя это отход от стандарта, оставляем как есть.
-
-**List<DateTime>** - означает **список** объектов типа **DateTime**
+> `List<DateTime>` - означает **список** объектов типа **DateTime**
 
 Для того, чтобы реализовать логику метода нам необходимо:
 
 * перебрать список входных данных (произволные даты)
 * отфильтровать даты по условию (только прошлый год)
 * сохранить дату в промежуточный список с датой первого числа месяца и количеством повторений этой даты
-* отсортировать промежуточный список по двум условиям: популярность месяца по убываению и, если популярность одинаковая, по месяцу по возрастанию
+* отсортировать промежуточный список по двум условиям: популярность месяца по убыванию и, если популярность одинаковая, по месяцу по возрастанию
 
 Для перебора элементов списка используется цикл **foreach**:
 
 ```cs
-// обращаем внимание на название переменных - IterDate (ПеребираемаяДата) пишем CamelCase-ом 
 foreach (DateTime IterDate in dates) {
     ...
 }
@@ -116,17 +112,19 @@ foreach (DateTime IterDate in dates) {
 Для вычисления прошлого года используем статический геттер класса **DateTime** *DateTime.Now*, который возвращает текущую дату и время. Нам остается получить у этого объекта год и вычесть единицу (естественно вычислять эту переменную нужно до цикла):
 
 ```cs
-int PreviousYear = DateTime.Now.Year-1;
-foreach (DateTime IterDate in dates) {
-    if (IterDate.Year == PreviousYear){
+int previousYear = DateTime.Now.Year-1;
+foreach (DateTime iterDate in dates) {
+    if (iterDate.Year == previousYear){
         ...
     }
 }
 ```
 
-**Следующий текст устарел! Использовать его можно, но не нужно. Лучше реализовать временный список с использованием кортежей. Это покажет ваше владение средствами языка и уменьшит количество кода/файлов.**
+<details>
 
-~~Теперь нам нужно объявить список объектов, в котором будет хранится дата и количество повторений этой даты. Для этого создадим класс **DateTimeWithCounter** (вообще, по современным гайдлайнам нужно каждый класс писать в отдельном файле и на демо-экзамене помните про это)~~
+<summary><b>Текст под спойлером устарел! Использовать его можно, но не нужно. Лучше реализовать временный список с использованием кортежей. Это покажет ваше владение средствами языка и уменьшит количество кода/файлов.</b></summary>
+
+Теперь нам нужно объявить список объектов, в котором будет хранится дата и количество повторений этой даты. Для этого создадим класс **DateTimeWithCounter** (вообще, по современным гайдлайнам нужно каждый класс писать в отдельном файле и на демо-экзамене помните про это)
 
 ```cs
 class DateTimeWithCounter
@@ -174,43 +172,60 @@ public class Analytics
 }
 ```
 
+</details>
+
+<br/>
+
 **Новый код с использованием кортежей**
 
 Теперь нам нужно объявить список объектов, в котором будет хранится дата и количество повторений этой даты. Для этого используем список [кортежей](https://metanit.com/sharp/tutorial/2.19.php)
 
 
 ```cs
-public List<DateTime> PopularMonths(List<DateTime> dates) {
-    var DateTimeWithCounterList = new List<Tuple<DateTime, int>>();
+public List<DateTime> PopularMonths(List<DateTime> dates) 
+{
+    var dateTimeWithCounterList = new List<Tuple<DateTime, int>>();
 
-    int PreviousYear = DateTime.Now.Year - 1;
-    foreach (DateTime IterDate in dates)
+    int previousYear = DateTime.Now.Year - 1;
+    foreach (DateTime iterDate in dates)
     {
-        if (IterDate.Year == PreviousYear)
+        if (iterDate.Year == previousYear)
         {
             // вычисляем начало месяца для текущей даты
-            var DateMonthStart = new DateTime(IterDate.Year, IterDate.Month, 1, 0, 0, 0);
+            var dateMonthStart = new DateTime(
+                iterDate.Year,  // год
+                iterDate.Month, // месяц
+                1, 0, 0, 0);    // день
 
             // ищем эту дату во временном списке
-            var index = DateTimeWithCounterList.FindIndex(item => item.Item1 == DateMonthStart);
+            var index = dateTimeWithCounterList
+                .FindIndex(
+                    item => item.Item1 == dateMonthStart);
 
             // кортежи можно создавать по-разному
             if (index == -1)
             {
                 // такой даты нет - добавляю (используя конструктор)
-                DateTimeWithCounterList.
-                    Add( new Tuple<DateTime,int>(DateMonthStart, 1) );
+                dateTimeWithCounterList.Add( 
+                    new Tuple<DateTime,int>(
+                        dateMonthStart, 1) );
             }
             else
             {
-                // дата есть - увеличиваем счетчик
-                // свойства кортежа неизменяемые, поэтому перезаписываем текущий элемент новым кортежем, который создаем статическим методом
-                DateTimeWithCounterList[index] = Tuple.Create(DateTimeWithCounterList[index].Item1, DateTimeWithCounterList[index].Item2 + 1);
+                /* 
+                    дата есть - увеличиваем счетчик
+                    свойства кортежа неизменяемые, 
+                    поэтому перезаписываем текущий элемент
+                    новым кортежем, 
+                    который создаем статическим методом
+                */
+                dateTimeWithCounterList[index] = Tuple.Create(
+                    dateTimeWithCounterList[index].Item1, dateTimeWithCounterList[index].Item2 + 1);
             }
         }
     }
 
-    return DateTimeWithCounterList
+    return dateTimeWithCounterList
         .OrderByDescending(item => item.Item2)
         .ThenBy(item => item.Item1)
         .Select(item => item.Item1)
@@ -218,7 +233,7 @@ public List<DateTime> PopularMonths(List<DateTime> dates) {
 }
 ```
 
-Для вычисления начала месяца мы использовали один из конструкторов класса **DateTime(year, month, day, hour, minutes, seconds)**, где год и месяц берем оригиналльные, а день равен "1"
+Для вычисления начала месяца мы использовали один из конструкторов класса **DateTime(year, month, day, hour, minutes, seconds)**, где год и месяц берем оригинальные, а день равен "1"
 
 Метод **FindIndex** ищет **позицию** (индекс) элемента в списке. Если элемент в списке есть, то возвращает целое от "0" и выше, а если нет, то "-1".
 
@@ -228,14 +243,14 @@ public List<DateTime> PopularMonths(List<DateTime> dates) {
 
 ```cs
 ...
-return DateTimeWithCounterList
+return dateTimeWithCounterList
     .OrderByDescending(item => item.Counter)
     .ThenBy(item => item.DateTimeProp)
     .Select(item => item.DateTimeProp)
     .ToList();
 ```
 
-**OrderByDescending** - сортирует исходный список по убыванию. В параметрах пишется лямбда функция, где *item* текущий элемент списка, а после "=>" свойство элемента, по которому производится сортировка. У еас по ТЗ первое условие сортировки по убыванию популярности, поэтому используем свойство Counter. Для прямой сортировки (по возрастанию) есть метод *OrderBy*.
+**OrderByDescending** - сортирует исходный список по убыванию. В параметрах пишется лямбда функция, где *item* текущий элемент списка, а после "=>" свойство элемента, по которому производится сортировка. У нас по ТЗ первое условие сортировки по убыванию популярности, поэтому используем свойство Counter. Для прямой сортировки (по возрастанию) есть метод *OrderBy*.
 
 **ThenBy** (ЗатемПо) - при совпадающих свойствах *популярность* сортировка будет производиться по полю *дата*. У метода *ThenBy* есть пара *ThenByDescending*. Методов *ThenBy* после *OrderBy* может быть несколько.
 
@@ -251,47 +266,57 @@ return DateTimeWithCounterList
 
 В контекстном меню решения добавьте новый проект
 
-![](../img/test_03.png)
+![](../img/rider019.png)
 
-Обратите внимание, тип приложения (Framework или Core) должен совпадать
+Обратите внимание, тип приложения (Framework или Core) должен совпадать с типом библиотеки
 
-![](../img/test_04.png)
+![](../img/rider020.png)
 
 В созданном приложении к контекстном меню "Зависимостей" добавьте ссылку на проект
 
-![](../img/test_05.png)
+![](../img/rider021.png)
 
-![](../img/test_06.png)
+![](../img/rider022.png)
 
 Теперь в коде консольного приложения мы можем использовать класс из библиотеки:
 
 ```cs
-namespace ConsoleApp1
-{
-    class Program
-    {
-        static void Main(string[] args)
-        {
-            // создаём экземпляр класса аналитики
-            var analytics = new Analytics();
+using CompanyCoreLib;
 
-            // подгатавливаем массив тестовых данных
-            var srcDates = new List<DateTime>()
-            {
-                new DateTime(2022,12,1,0,0,0),
-                new DateTime(2022,11,1,0,0,0),
-            };
+// создаём экземпляр класса аналитики
+var analytics = new Analytics();
 
-            // выполняем метод и получаем результат
-            var outDates = analytics.PopularMonths(srcDates);
+// подгатавливаем массив тестовых данных
+var srcDates = new List<DateTime>()
+{
+    new DateTime(2022,12,1,0,0,0),
+    new DateTime(2022,11,1,0,0,0),
+};
 
-            // вывод результата в консоль сделайте сами
-        }
-    }
+// выполняем метод и получаем результат
+var outDates = analytics.PopularMonths(srcDates);
+
+// выводим результат в консоль
+foreach (var date in outDates)
+{
+    Console.WriteLine(date.ToString());    
 }
 ```
 
-# Контрольное задание
+У меня получилось так:
+
+```
+01.11.2023 00:00:00
+01.12.2023 00:00:00
+```
+
+---
+
+## Контрольное задание
 
 * Реализовать ТЗ, написанное в начале этой лекции. 
-* Результат оформить в виде репозитория (как написано в ТЗ). Обязательно с файлом `readme.md`
+* Результат оформить в виде репозитория (как написано в ТЗ). Обязательно с файлом `readme.md`
+
+Предыдущая лекция | &nbsp; | Следующая лекция
+:----------------:|:----------:|:----------------:
+[Реинжиниринг бизнес-процессов в информационных системах.](./5_3_1_8_reengeniring.md) | [Содержание](../readme.md#мдк-0503-тестирование-информационных-систем) | [Создание UNIT-тестов](./5_3_1_10_unit_test.md)

+ 11 - 11
articles/fake_unit_test.md

@@ -1,10 +1,6 @@
-<table style="width: 100%;"><tr><td style="width: 40%;">
-<a href="../articles/5_3_1_10_unit_test.md">Создание UNIT-тестов
-</a></td><td style="width: 20%;">
-<a href="../readme.md">Содержание
-</a></td><td style="width: 40%;">
-<a href="../articles/fake_unit_test.md">Fake data. Тестирование методов получающих внешние данные из удалённых источников.
-</a></td><tr></table>
+Предыдущая лекция | &nbsp; | Следующая лекция
+:----------------:|:----------:|:----------------:
+[Создание UNIT-тестов](./5_3_1_10_unit_test.md) | [Содержание](../readme.md#мдк-0503-тестирование-информационных-систем) | &nbsp;
 
 # Fake data. Тестирование методов получающих внешние данные из удалённых источников.
 
@@ -16,10 +12,14 @@
 
 1. Вынесите все классы модели, а также класс **Global** и интерфейс **IDataProvider** в библиотеку классов **DataProviderLib** (проект создавайте в этом же решении)
 
-2. В основной проект добавьте ссылку на созданную библиотеку.
+1. В основной проект добавьте ссылку на созданную библиотеку.
 
-3. Бизнес-логику удаления/добавления записей перенестие в классы моделей.
+1. Бизнес-логику удаления/добавления записей перенестие в классы моделей.
 
-4. Добавьте в решение ещё одну библиотеку классов **FakeDataProviderLib**, в которой реализуйте класс FakeDataProvider, наследующий интерфейс **IDataProvider** (соответственно, в неё тоже нужно добавить ссылку на **DataProviderLib**)
+1. Добавьте в решение ещё одну библиотеку классов **FakeDataProviderLib**, в которой реализуйте класс FakeDataProvider, наследующий интерфейс **IDataProvider** (соответственно, в неё тоже нужно добавить ссылку на **DataProviderLib**)
 
-5. В решение добавьте проект UNIT-тестов и добавьте в него ссылки на библиотеки **DataProviderLib** и **FakeDataProviderLib**. Напишите тесты для добавления и удаления продукции. При инициализации тестов Globals.DataProvider присваиваем наш FakeDateProvider.
+1. В решение добавьте проект UNIT-тестов и добавьте в него ссылки на библиотеки **DataProviderLib** и **FakeDataProviderLib**. Напишите тесты для добавления и удаления продукции. При инициализации тестов Globals.DataProvider присваиваем наш FakeDateProvider.
+
+Предыдущая лекция | &nbsp; | Следующая лекция
+:----------------:|:----------:|:----------------:
+[Создание UNIT-тестов](./5_3_1_10_unit_test.md) | [Содержание](../readme.md#мдк-0503-тестирование-информационных-систем) | &nbsp;

BIN
img/rider016.png


BIN
img/rider017.png


BIN
img/rider018.png


BIN
img/rider019.png


BIN
img/rider020.png


BIN
img/rider021.png


BIN
img/rider022.png


BIN
img/rider023.png


BIN
img/rider024.png


BIN
img/rider025.png