diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
new file mode 100644
index 0000000..e940250
--- /dev/null
+++ b/.github/workflows/dotnet.yml
@@ -0,0 +1,23 @@
+name: .NET
+
+on:
+ push:
+ branches: [ "master" ]
+ pull_request:
+ branches: [ "master" ]
+
+jobs:
+ build-and-tests:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.0.x
+ - name: Restore dependencies
+ run: dotnet restore ./Testing/Testing.sln
+ - name: Build
+ run: dotnet build --no-restore ./Testing/Testing.sln
+ - name: Unit Test
+ run: dotnet test --no-build --verbosity normal ./Testing/Testing.sln
\ No newline at end of file
diff --git a/Testing/Advanced/Advanced.csproj b/Testing/Advanced/Advanced.csproj
new file mode 100644
index 0000000..1a7435e
--- /dev/null
+++ b/Testing/Advanced/Advanced.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Testing/Advanced/Classwork/1. ThingCache/IThingService.cs b/Testing/Advanced/Classwork/1. ThingCache/IThingService.cs
new file mode 100644
index 0000000..a0ad3a0
--- /dev/null
+++ b/Testing/Advanced/Classwork/1. ThingCache/IThingService.cs
@@ -0,0 +1,6 @@
+namespace Advanced.Classwork.ThingCache;
+
+public interface IThingService
+{
+ bool TryRead(string thingId, out Thing value);
+}
\ No newline at end of file
diff --git a/Testing/Advanced/Classwork/1. ThingCache/Readme.md b/Testing/Advanced/Classwork/1. ThingCache/Readme.md
new file mode 100644
index 0000000..8b59bc0
--- /dev/null
+++ b/Testing/Advanced/Classwork/1. ThingCache/Readme.md
@@ -0,0 +1,4 @@
+Есть сервис IThingService, у которого можно получить описание предметов
+К нему по возможности надо обращаться как можно реже, поэтому был реализован кэш ThingCache
+
+Напишите тесты на ThingCache, используя FakeItEasy для подмены IThingService
diff --git a/Testing/Advanced/Classwork/1. ThingCache/Thing.cs b/Testing/Advanced/Classwork/1. ThingCache/Thing.cs
new file mode 100644
index 0000000..8a166ce
--- /dev/null
+++ b/Testing/Advanced/Classwork/1. ThingCache/Thing.cs
@@ -0,0 +1,14 @@
+namespace Advanced.Classwork.ThingCache;
+
+public class Thing
+{
+ public string ThingId { get; set; }
+ public string? Name { get; set; }
+ public string? Description { get; set; }
+
+ public Thing(string thingId)
+ {
+ ThingId = thingId;
+ }
+
+}
\ No newline at end of file
diff --git a/Testing/Advanced/Classwork/1. ThingCache/ThingCache.cs b/Testing/Advanced/Classwork/1. ThingCache/ThingCache.cs
new file mode 100644
index 0000000..2da59ef
--- /dev/null
+++ b/Testing/Advanced/Classwork/1. ThingCache/ThingCache.cs
@@ -0,0 +1,39 @@
+using ApprovalUtilities.SimpleLogger;
+using log4net;
+using log4net.Core;
+
+namespace Advanced.Classwork.ThingCache;
+
+public class ThingCache
+{
+ private static readonly ILog logger = LogManager.GetLogger(typeof(ThingCache));
+
+
+ private readonly IDictionary dictionary
+ = new Dictionary();
+ private readonly IThingService thingService;
+
+ public ThingCache(IThingService thingService)
+ {
+ this.thingService = thingService;
+ }
+
+ public Thing Get(string thingId)
+ {
+ Thing thing;
+ logger.Info($"Try get by thingId=[{thingId}]");
+ if (dictionary.TryGetValue(thingId, out thing))
+ {
+ logger.Info($"Find thing in cache");
+ return thing;
+ }
+ if (thingService.TryRead(thingId, out thing))
+ {
+ logger.Info($"Find thing in service");
+ dictionary[thingId] = thing;
+ return thing;
+ }
+ logger.Info($"Not found thing");
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/Testing/Advanced/Classwork/1. ThingCache/ThingCacheTests.cs b/Testing/Advanced/Classwork/1. ThingCache/ThingCacheTests.cs
new file mode 100644
index 0000000..56a3d94
--- /dev/null
+++ b/Testing/Advanced/Classwork/1. ThingCache/ThingCacheTests.cs
@@ -0,0 +1,61 @@
+using NUnit.Framework;
+
+namespace Advanced.Classwork.ThingCache;
+
+
+[TestFixture]
+public class ThingCache_Should
+{
+ private IThingService thingService;
+ private ThingCache thingCache;
+
+ private const string thingId1 = "TheDress";
+ private Thing thing1 = new Thing(thingId1);
+
+ private const string thingId2 = "CoolBoots";
+ private Thing thing2 = new Thing(thingId2);
+
+ // Метод, помеченный атрибутом SetUp, выполняется перед каждым тестов
+ [SetUp]
+ public void SetUp()
+ {
+ //thingService = A...
+ thingCache = new ThingCache(thingService);
+ }
+
+ // TODO: Написать простейший тест, а затем все остальные
+
+ // Пример теста
+ [Test]
+ public void GiveMeAGoodNamePlease()
+ {
+ }
+
+ /** Проверки в тестах
+ * Assert.AreEqual(expectedValue, actualValue);
+ * actualValue.Should().Be(expectedValue);
+ */
+
+ /** Синтаксис AAA
+ * Arrange:
+ * var fake = A.Fake();
+ * A.CallTo(() => fake.SomeMethod(...)).Returns(true);
+ * Assert:
+ * var value = "42";
+ * A.CallTo(() => fake.TryRead(id, out value)).MustHaveHappened();
+ */
+
+ /** Синтаксис out
+ * var value = "42";
+ * string _;
+ * A.CallTo(() => fake.TryRead(id, out _)).Returns(true)
+ * .AssignsOutAndRefParameters(value);
+ * A.CallTo(() => fake.TryRead(id, out value)).Returns(true);
+ */
+
+ /** Синтаксис Repeat
+ * var value = "42";
+ * A.CallTo(() => fake.TryRead(id, out value))
+ * .MustHaveHappened(Repeated.Exactly.Twice)
+ */
+}
\ No newline at end of file
diff --git a/Testing/Advanced/Classwork/2. FileSender/Dependecies/Document.cs b/Testing/Advanced/Classwork/2. FileSender/Dependecies/Document.cs
new file mode 100644
index 0000000..e79db51
--- /dev/null
+++ b/Testing/Advanced/Classwork/2. FileSender/Dependecies/Document.cs
@@ -0,0 +1,5 @@
+using System;
+
+namespace Advanced.Classwork.FileSender.Dependecies;
+
+public record Document(string Name, byte[] Content, DateTime Created, string Format);
\ No newline at end of file
diff --git a/Testing/Advanced/Classwork/2. FileSender/Dependecies/File.cs b/Testing/Advanced/Classwork/2. FileSender/Dependecies/File.cs
new file mode 100644
index 0000000..61cb606
--- /dev/null
+++ b/Testing/Advanced/Classwork/2. FileSender/Dependecies/File.cs
@@ -0,0 +1,3 @@
+namespace Advanced.Classwork.FileSender.Dependecies;
+
+public record File(string Name, byte[] Content);
\ No newline at end of file
diff --git a/Testing/Advanced/Classwork/2. FileSender/Dependecies/ICryptographer.cs b/Testing/Advanced/Classwork/2. FileSender/Dependecies/ICryptographer.cs
new file mode 100644
index 0000000..f249e03
--- /dev/null
+++ b/Testing/Advanced/Classwork/2. FileSender/Dependecies/ICryptographer.cs
@@ -0,0 +1,8 @@
+using System.Security.Cryptography.X509Certificates;
+
+namespace Advanced.Classwork.FileSender.Dependecies;
+
+public interface ICryptographer
+{
+ byte[] Sign(byte[] content, X509Certificate certificate);
+}
\ No newline at end of file
diff --git a/Testing/Advanced/Classwork/2. FileSender/Dependecies/IRecognizer.cs b/Testing/Advanced/Classwork/2. FileSender/Dependecies/IRecognizer.cs
new file mode 100644
index 0000000..7acc63b
--- /dev/null
+++ b/Testing/Advanced/Classwork/2. FileSender/Dependecies/IRecognizer.cs
@@ -0,0 +1,6 @@
+namespace Advanced.Classwork.FileSender.Dependecies;
+
+public interface IRecognizer
+{
+ bool TryRecognize(File file, out Document document);
+}
diff --git a/Testing/Advanced/Classwork/2. FileSender/Dependecies/ISender.cs b/Testing/Advanced/Classwork/2. FileSender/Dependecies/ISender.cs
new file mode 100644
index 0000000..2052436
--- /dev/null
+++ b/Testing/Advanced/Classwork/2. FileSender/Dependecies/ISender.cs
@@ -0,0 +1,6 @@
+namespace Advanced.Classwork.FileSender.Dependecies;
+
+public interface ISender
+{
+ bool TrySend(byte[] content);
+}
\ No newline at end of file
diff --git a/Testing/Advanced/Classwork/2. FileSender/FileSender.cs b/Testing/Advanced/Classwork/2. FileSender/FileSender.cs
new file mode 100644
index 0000000..1802c9a
--- /dev/null
+++ b/Testing/Advanced/Classwork/2. FileSender/FileSender.cs
@@ -0,0 +1,58 @@
+using Advanced.Classwork.FileSender.Dependecies;
+using System.Security.Cryptography.X509Certificates;
+using File = Advanced.Classwork.FileSender.Dependecies.File;
+
+namespace Advanced.Classwork.FileSender;
+
+public class FileSender
+{
+ private readonly ICryptographer cryptographer;
+ private readonly ISender sender;
+ private readonly IRecognizer recognizer;
+
+ public FileSender(
+ ICryptographer cryptographer,
+ ISender sender,
+ IRecognizer recognizer)
+ {
+ this.cryptographer = cryptographer;
+ this.sender = sender;
+ this.recognizer = recognizer;
+ }
+
+ public Result SendFiles(File[] files, X509Certificate certificate)
+ {
+ return new Result
+ {
+ SkippedFiles = files
+ .Where(file => !TrySendFile(file, certificate))
+ .ToArray()
+ };
+ }
+
+ private bool TrySendFile(File file, X509Certificate certificate)
+ {
+ Document document;
+ if (!recognizer.TryRecognize(file, out document))
+ return false;
+ if (!CheckFormat(document) || !CheckActual(document))
+ return false;
+ var signedContent = cryptographer.Sign(document.Content, certificate);
+ return sender.TrySend(signedContent);
+ }
+
+ private bool CheckFormat(Document document)
+ {
+ return document.Format == "4.0" || document.Format == "3.1";
+ }
+
+ private bool CheckActual(Document document)
+ {
+ return document.Created.AddMonths(1) > DateTime.Now;
+ }
+
+ public class Result
+ {
+ public File[] SkippedFiles { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Testing/Advanced/Classwork/2. FileSender/FileSenderTests.cs b/Testing/Advanced/Classwork/2. FileSender/FileSenderTests.cs
new file mode 100644
index 0000000..4dc58c1
--- /dev/null
+++ b/Testing/Advanced/Classwork/2. FileSender/FileSenderTests.cs
@@ -0,0 +1,105 @@
+using Advanced.Classwork.FileSender.Dependecies;
+using FakeItEasy;
+using FluentAssertions;
+using NUnit.Framework;
+using System.Security.Cryptography.X509Certificates;
+using Document = Advanced.Classwork.FileSender.Dependecies.Document;
+using File = Advanced.Classwork.FileSender.Dependecies.File;
+
+namespace Advanced.Classwork.FileSender;
+
+//TODO: реализовать недостающие тесты
+[TestFixture]
+public class FileSender_Should
+{
+ private FileSender fileSender;
+ private ICryptographer cryptographer;
+ private ISender sender;
+ private IRecognizer recognizer;
+
+ private readonly X509Certificate certificate = new X509Certificate();
+ private File file;
+ private byte[] signedContent;
+
+ [SetUp]
+ public void SetUp()
+ {
+ // Постарайтесь вынести в SetUp всё неспецифическое конфигурирование так,
+ // чтобы в конкретных тестах осталась только специфика теста,
+ // без конфигурирования "обычного" сценария работы
+
+ file = new File("someFile", new byte[] { 1, 2, 3 });
+ signedContent = new byte[] { 1, 7 };
+
+ cryptographer = A.Fake();
+ sender = A.Fake();
+ recognizer = A.Fake();
+ fileSender = new FileSender(cryptographer, sender, recognizer);
+ }
+
+ [TestCase("4.0")]
+ [TestCase("3.1")]
+ public void Send_WhenGoodFormat(string format)
+ {
+ var document = new Document(file.Name, file.Content, DateTime.Now, format);
+ A.CallTo(() => recognizer.TryRecognize(file, out document))
+ .Returns(true);
+ A.CallTo(() => cryptographer.Sign(document.Content, certificate))
+ .Returns(signedContent);
+ A.CallTo(() => sender.TrySend(signedContent))
+ .Returns(true);
+
+ fileSender.SendFiles(new[] { file }, certificate)
+ .SkippedFiles
+ .Should().BeEmpty();
+ }
+
+ [Test]
+ [Ignore("Not implemented")]
+ public void Skip_WhenBadFormat()
+ {
+ throw new NotImplementedException();
+ }
+
+ [Test]
+ [Ignore("Not implemented")]
+ public void Skip_WhenOlderThanAMonth()
+ {
+ throw new NotImplementedException();
+ }
+
+ [Test]
+ [Ignore("Not implemented")]
+ public void Send_WhenYoungerThanAMonth()
+ {
+ throw new NotImplementedException();
+ }
+
+ [Test]
+ [Ignore("Not implemented")]
+ public void Skip_WhenSendFails()
+ {
+ throw new NotImplementedException();
+ }
+
+ [Test]
+ [Ignore("Not implemented")]
+ public void Skip_WhenNotRecognized()
+ {
+ throw new NotImplementedException();
+ }
+
+ [Test]
+ [Ignore("Not implemented")]
+ public void IndependentlySend_WhenSeveralFilesAndSomeAreInvalid()
+ {
+ throw new NotImplementedException();
+ }
+
+ [Test]
+ [Ignore("Not implemented")]
+ public void IndependentlySend_WhenSeveralFilesAndSomeCouldNotSend()
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/Testing/Advanced/Classwork/2. FileSender/Readme.md b/Testing/Advanced/Classwork/2. FileSender/Readme.md
new file mode 100644
index 0000000..85ba91d
--- /dev/null
+++ b/Testing/Advanced/Classwork/2. FileSender/Readme.md
@@ -0,0 +1,5 @@
+На метод SendFiles написан только один тест, проверяющий успешную отправку файлов.
+
+Надо реализовать оставшиеся тесты на метод SendFiles класса FileSender
+
+Нельзя менять файлы из папки Dependencies!
diff --git a/Testing/Advanced/Classwork/3. ApprovalsTests/ApprovalsTests.cs b/Testing/Advanced/Classwork/3. ApprovalsTests/ApprovalsTests.cs
new file mode 100644
index 0000000..c9e2e87
--- /dev/null
+++ b/Testing/Advanced/Classwork/3. ApprovalsTests/ApprovalsTests.cs
@@ -0,0 +1,65 @@
+using Newtonsoft.Json;
+using NUnit.Framework;
+
+namespace Advanced.Classwork.ApprovalsTests;
+
+[TestFixture]
+[Explicit]
+public class ApprovalsTests
+{
+ [Test]
+ public void Puzzle15_InitialState()
+ {
+ var puzzle15 = new Puzzle15();
+ // TODO: assert
+ // HINT: Approvals.Verify
+ }
+
+ #region Как это работает
+
+ // DiffReporter - выбирает наилучший имеющийся в наличии способ сравнения
+ // Approvals.Verify создает файл *.received.txt с текущим значением и сравнивает его с файлом *.approved.txt
+
+ #endregion
+
+ [Test]
+ public void Puzzle15_MoveRight()
+ {
+ var puzzle15 = new Puzzle15();
+ puzzle15.MoveRight();
+
+ // TODO: assert
+ }
+
+ [Test]
+ public void ApproveProductData()
+ {
+ var product = new Product
+ {
+ Id = Guid.Empty,
+ Name = "Name",
+ Price = 3.14m,
+ UnitsCode = "112"
+ };
+ //TODO: Verify product
+ //TODO: Exclude TemporaryData
+ //HINT: stateprinter.Configuration.Project.Exclude
+ }
+
+ [Test]
+ public void ProductData_IsJsonSerializable()
+ {
+ Product original = new Product
+ {
+ Id = Guid.Empty,
+ Name = "Name",
+ Price = 3.14m,
+ UnitsCode = "112",
+ TemporaryData = "qwe"
+ };
+ string serialized = JsonConvert.SerializeObject(original);
+ Product deserialized = JsonConvert.DeserializeObject(serialized);
+ //TODO: Проверить, что сериализуется корректно!
+ //HINT: Should().BeEquivalentTo с опциями в FluentAssertions
+ }
+}
diff --git a/Testing/Advanced/Classwork/3. ApprovalsTests/Product.cs b/Testing/Advanced/Classwork/3. ApprovalsTests/Product.cs
new file mode 100644
index 0000000..5f45a82
--- /dev/null
+++ b/Testing/Advanced/Classwork/3. ApprovalsTests/Product.cs
@@ -0,0 +1,13 @@
+using System.Text.Json.Serialization;
+
+namespace Advanced.Classwork.ApprovalsTests;
+
+public class Product
+{
+ public Guid Id { get; set; }
+ [JsonIgnore]
+ public string TemporaryData { get; set; }
+ public string Name { get; set; }
+ public decimal Price { get; set; }
+ public string UnitsCode { get; set; }
+}
\ No newline at end of file
diff --git a/Testing/Advanced/Classwork/3. ApprovalsTests/Puzzle15.cs b/Testing/Advanced/Classwork/3. ApprovalsTests/Puzzle15.cs
new file mode 100644
index 0000000..99b6f46
--- /dev/null
+++ b/Testing/Advanced/Classwork/3. ApprovalsTests/Puzzle15.cs
@@ -0,0 +1,58 @@
+using System.Drawing;
+
+namespace Advanced.Classwork.ApprovalsTests;
+
+public class Puzzle15
+{
+ private readonly int[,] map = new int[4, 4];
+ private Point empty;
+
+ public Puzzle15(int[,] map)
+ {
+ if (map.GetLength(0) != 4 || map.GetLength(1) != 4)
+ throw new ArgumentException("should be 4x4", nameof(map));
+ this.map = (int[,])map.Clone();
+ }
+
+ public Puzzle15()
+ {
+ var i = 0;
+ empty = new Point(0, 0);
+ for (int y = 0; y < 4; y++)
+ for (int x = 0; x < 4; x++)
+ map[y, x] = i++;
+ }
+
+ public int this[Point pos]
+ {
+ get => map[pos.Y, pos.X];
+ set { map[pos.Y, pos.X] = value; }
+ }
+
+ public override string ToString() =>
+ string.Join("\r\n", Enumerable.Range(0, 4).Select(FormatLine));
+
+ private string FormatLine(int y)
+ {
+ var cells = Enumerable.Range(0, 4).Select(x => map[y, x].ToString().PadLeft(2));
+ return string.Join(" ", cells);
+ }
+
+ public void MoveLeft() => Move(-1, 0);
+ public void MoveRight() => Move(1, 0);
+ public void MoveUp() => Move(0, -1);
+ public void MoveDown() => Move(0, 1);
+
+ public void Move(int dx, int dy)
+ {
+ var newEmpty = empty + new Size(dx, dy);
+ if (newEmpty.X >= 0 && newEmpty.X < 4 &&
+ newEmpty.Y >= 0 && newEmpty.Y < 4)
+ {
+ var t = this[empty];
+ this[empty] = this[newEmpty];
+ this[newEmpty] = t;
+ empty = newEmpty;
+ }
+ }
+}
diff --git a/Testing/Advanced/Classwork/3. ApprovalsTests/Readme.md b/Testing/Advanced/Classwork/3. ApprovalsTests/Readme.md
new file mode 100644
index 0000000..17f3010
--- /dev/null
+++ b/Testing/Advanced/Classwork/3. ApprovalsTests/Readme.md
@@ -0,0 +1 @@
+Нужно реализовать характеризационные тесты в файле ApprovalsTests.cs
\ No newline at end of file
diff --git a/Testing/Advanced/Infrastructure/AutoApproveReporter.cs b/Testing/Advanced/Infrastructure/AutoApproveReporter.cs
new file mode 100644
index 0000000..15f3713
--- /dev/null
+++ b/Testing/Advanced/Infrastructure/AutoApproveReporter.cs
@@ -0,0 +1,39 @@
+using ApprovalTests.Core;
+using System.Diagnostics;
+
+namespace Advanced.Infrastructure;
+
+/*
+При написании характеризационных тестов на работающий код
+может возникнуть желание заапрувить множество тестов сразу.
+Для этого можно написать специальный Reporter.
+
+Его нельзя использовать после, иначе ваши тесты будут вечнозелеными и бесполезными!
+
+Код взят тут: https://stackoverflow.com/questions/37604285/how-do-i-automatically-approve-approval-tests-when-i-run-them
+*/
+public class AutoApproveReporter : IReporterWithApprovalPower
+{
+ public static readonly AutoApproveReporter INSTANCE = new AutoApproveReporter();
+
+ private string approved;
+ private string received;
+
+ public void Report(string approved, string received)
+ {
+ this.approved = approved;
+ this.received = received;
+ Trace.WriteLine(string.Format(@"Will auto-copy ""{0}"" to ""{1}""", received, approved));
+ }
+
+ public bool ApprovedWhenReported()
+ {
+ if (!File.Exists(received)) return false;
+ File.Delete(approved);
+ if (File.Exists(approved)) return false;
+ File.Copy(received, approved);
+ if (!File.Exists(approved)) return false;
+
+ return true;
+ }
+}
diff --git a/Testing/Advanced/Readme.md b/Testing/Advanced/Readme.md
new file mode 100644
index 0000000..e66360d
--- /dev/null
+++ b/Testing/Advanced/Readme.md
@@ -0,0 +1,37 @@
+# Тестирование. Часть 2
+
+Пройдя блок, ты:
+
+- научишься использовать моки в тестировании
+- узнаешь как выглядит паттерн AAA в тестах с моками
+- научишься писать Approval Tests для фиксации текущего поведения кода
+- научишься писать функциональные тесты с помощью Silenium
+- Познакомишься с CI CD
+
+
+## Необходимые знания
+
+Понадобится знание C#
+
+Рекомендуется пройти блоки [Тестирование](https://github.com/kontur-courses/testing) и [Dependency Injection Container](https://github.com/kontur-courses/di)
+
+
+## Самостоятельная подготовка
+
+Посмотри видеолекцию [Mock-библиотеки](https://ulearn.me/Course/cs2/Mock_bibliotieki_dbfc7c12-41f2-4205-ad4d-9283f9f5d3f4) (~15 мин.)
+
+
+## Очная встреча
+
+~ 3 часа
+
+
+## Закрепление материала
+
+1. Спецзадание __No Mocks__
+Найди в своем проекте тесты, активно использующие какую-либо Mock-библиотеку. Подумай как можно было бы написать эти тесты без mock-ов? В каких случаях mock-и необходимы?
+
+
+## Дополнительные ссылки
+
+- [Mocks Aren't Stubs](https://martinfowler.com/articles/mocksArentStubs.html) - статья от Боба Мартина о том, как увлечение "поведенческим тестированием" и моками влияет на стиль кода
diff --git a/Testing/Advanced/Samples/1. Mocks/Dto.cs b/Testing/Advanced/Samples/1. Mocks/Dto.cs
new file mode 100644
index 0000000..f269d4c
--- /dev/null
+++ b/Testing/Advanced/Samples/1. Mocks/Dto.cs
@@ -0,0 +1,16 @@
+namespace Advanced.Samples.Mocks;
+
+public record Dto(string S);
+public class ComplexDto
+{
+ public readonly Dto? dto;
+ public readonly string? s;
+
+ public ComplexDto() { }
+
+ public ComplexDto(Dto dto)
+ {
+ this.dto = dto;
+ s = "Created with complex constructor";
+ }
+}
\ No newline at end of file
diff --git a/Testing/Advanced/Samples/1. Mocks/IService.cs b/Testing/Advanced/Samples/1. Mocks/IService.cs
new file mode 100644
index 0000000..b504dc9
--- /dev/null
+++ b/Testing/Advanced/Samples/1. Mocks/IService.cs
@@ -0,0 +1,7 @@
+namespace Advanced.Samples.Mocks;
+
+public interface IService
+{
+ string Get();
+ bool TryGet(string id, out string value);
+}
diff --git a/Testing/Advanced/Samples/1. Mocks/Mocks.cs b/Testing/Advanced/Samples/1. Mocks/Mocks.cs
new file mode 100644
index 0000000..8c85a82
--- /dev/null
+++ b/Testing/Advanced/Samples/1. Mocks/Mocks.cs
@@ -0,0 +1,77 @@
+using FakeItEasy;
+using FluentAssertions;
+using NUnit.Framework;
+
+namespace Advanced.Samples.Mocks;
+
+
+[TestFixture]
+public class Mocks
+{
+ [Test]
+ public void Fail_OnNotConfiguredCalls_InStrictMode()
+ {
+ var service = A.Fake(o => o.Strict());
+ Assert.Throws(
+ () => service.Get());
+
+ }
+ [Test]
+ public void ReturnsDefault_AfterSequenceEnds()
+ {
+ var service = A.Fake();
+ A.CallTo(() => service.Get())
+ .ReturnsNextFromSequence("1", "2");
+ service.Get().Should().Be("1");
+ service.Get().Should().Be("2");
+ service.Get().Should().Be("");
+ }
+
+ [Test]
+ public void Creates_ObjectWithParameterlessConstructor()
+ {
+ var func = A.Fake>();
+ var complexDto = func();
+ complexDto.Should().NotBeNull();
+ complexDto.dto.Should().BeNull();
+ complexDto.s.Should().BeNull();
+ }
+
+ [Test]
+ public void ReturnsOnce_HasStackBehaviour()
+ {
+ var service = A.Fake();
+ A.CallTo(() => service.Get()).Returns("1").Once();
+ A.CallTo(() => service.Get()).Returns("2").Once();
+ service.Get().Should().Be("2");
+ A.CallTo(() => service.Get()).Returns("3").Once();
+ service.Get().Should().Be("3");
+ service.Get().Should().Be("1");
+ service.Get().Should().Be("");
+ }
+
+ [Test]
+ public void MustNotHaveHappened()
+ {
+ var service = A.Fake();
+
+
+ A.CallTo(() => service.Get())
+ .MustNotHaveHappened();
+ }
+
+ [Test]
+ public void OutParameters()
+ {
+ var service = A.Fake();
+ var id = "id";
+ string result = "42";
+
+ A.CallTo(() => service.TryGet(id, out result))
+ .Returns(true);
+
+ service.TryGet(id, out var actualResult).Should().BeTrue();
+ actualResult.Should().Be(result);
+
+ }
+}
diff --git a/Testing/Advanced/Samples/2. ApprovalsTests/LogTricks.cs b/Testing/Advanced/Samples/2. ApprovalsTests/LogTricks.cs
new file mode 100644
index 0000000..f59fe6e
--- /dev/null
+++ b/Testing/Advanced/Samples/2. ApprovalsTests/LogTricks.cs
@@ -0,0 +1,40 @@
+using Advanced.Classwork.ThingCache;
+using ApprovalTests;
+using ApprovalTests.Reporters;
+using FakeItEasy;
+using log4net.Appender;
+using log4net.Config;
+using NUnit.Framework;
+
+namespace Advanced.Samples.ApprovalsTests;
+
+[TestFixture]
+[Explicit]
+internal class LogTricks
+{
+ [Test]
+ [UseReporter(typeof(DiffReporter))]
+ public void Log()
+ {
+ // перехватываем записи логгера в память
+ var memoryAppender = new MemoryAppender();
+ BasicConfigurator.Configure(memoryAppender);
+
+ var id = "42";
+ Thing result = null;
+
+ var service = A.Fake();
+ A.CallTo(() => service.TryRead(id, out result)).Returns(false);
+
+ var cache = new ThingCache(service);
+
+ cache.Get(id);
+
+ var logs = memoryAppender
+ .GetEvents()
+ .Select(x => x.RenderedMessage);
+
+
+ Approvals.Verify(string.Join("\n", logs));
+ }
+}
diff --git a/Testing/Advanced/Samples/2. ApprovalsTests/PairwiseTests.cs b/Testing/Advanced/Samples/2. ApprovalsTests/PairwiseTests.cs
new file mode 100644
index 0000000..3847c5b
--- /dev/null
+++ b/Testing/Advanced/Samples/2. ApprovalsTests/PairwiseTests.cs
@@ -0,0 +1,49 @@
+using ApprovalTests;
+using ApprovalTests.Combinations;
+using ApprovalTests.Reporters;
+using NUnit.Framework;
+
+namespace Advanced.Samples.ApprovalsTests;
+
+[TestFixture]
+[Explicit]
+internal class PairwiseTests
+{
+ [Test, Combinatorial]
+ public void CombinatorialConsole(
+ [Values("a", "b", "c")] string a,
+ [Values("+", "-")] string b,
+ [Values("x", "y")] string c)
+ {
+ Console.WriteLine("{0} {1} {2}", a, b, c);
+ }
+
+ [Test, Pairwise]
+ public void PairwiseConsole(
+ [Values("a", "b", "c")] string a,
+ [Values("+", "-")] string b,
+ [Values("x", "y")] string c)
+ {
+ Console.WriteLine("{0} {1} {2}", a, b, c);
+ }
+
+ [Test, Pairwise]
+ public void PairwiseApprovals(
+ [Values("a", "b", "c")] string a,
+ [Values("+", "-")] string b,
+ [Values("x", "y")] string c)
+ {
+ Approvals.Verify($"{a} {b} {c}");
+ }
+
+
+ [Test]
+ [UseReporter(typeof(DiffReporter))]
+ public void CombinatorialApprovals()
+ {
+ CombinationApprovals.VerifyAllCombinations(
+ (a, b) => a + b,
+ new[] { 1, 2, 3 },
+ new[] { 0, -1, -5 });
+ }
+}
\ No newline at end of file
diff --git a/Testing/Advanced/Samples/3. InterfaceTests/Silenium.cs b/Testing/Advanced/Samples/3. InterfaceTests/Silenium.cs
new file mode 100644
index 0000000..6d8a652
--- /dev/null
+++ b/Testing/Advanced/Samples/3. InterfaceTests/Silenium.cs
@@ -0,0 +1,63 @@
+using FluentAssertions;
+using NUnit.Framework;
+using OpenQA.Selenium;
+using OpenQA.Selenium.Chrome;
+
+namespace Advanced.Samples.Interface;
+
+[TestFixture]
+[Explicit]
+internal class Silenium
+{
+ private ChromeDriver webDriver;
+
+ [SetUp]
+ public void SetUp()
+ {
+ webDriver = new ChromeDriver();
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ webDriver.Dispose();
+ }
+
+ /*
+ * Для поиска местоположения веб-элемента из DOM используются локатор.
+ * Дальнейшее взаимодейтействием выполняется относительно найденого элемента.
+ * Несколько популярных локаторов в Selenium - ID, Name, Link Text, Partial Link Text, CSS Selectors, XPath, TagName и т.д.
+ */
+
+ [Test]
+ public void Google()
+ {
+ webDriver.Url = "https://www.google.com";
+
+ /*
+ * В HTML у инпута поиска такая верстка:
+ *
+ */
+ var searchControl = webDriver.FindElement(By.Name("q"));
+
+ searchControl.SendKeys("Контур");
+ searchControl.SendKeys(Keys.Enter);
+
+ webDriver.Title.Should().Be("Контур - Поиск в Google");
+ }
+
+ [Test]
+ public void Wikipedia_KonturCreatedDate_ShouldBe1988()
+ {
+ webDriver.Url = "https://www.wikipedia.org/";
+
+ var searchControl = webDriver.FindElement(By.Name("search"));
+ searchControl.SendKeys("СКБ Контур");
+ searchControl.SendKeys(Keys.Enter);
+
+ // Как получился такой локатор? Стоит ли использовать такие локаторы для тестирования?
+ var locator = By.CssSelector("#mw-content-text > div.mw-content-ltr.mw-parser-output > table.infobox.infobox-3578c39699877354 > tbody > tr:nth-child(5)");
+ var createdYearCell = webDriver.FindElement(locator);
+ createdYearCell.Text.Should().Contain("Основание 1988");
+ }
+}
\ No newline at end of file
diff --git a/Testing/Advanced/Samples/4. PerformanceTests/Benchmark.cs b/Testing/Advanced/Samples/4. PerformanceTests/Benchmark.cs
new file mode 100644
index 0000000..6fb8f8d
--- /dev/null
+++ b/Testing/Advanced/Samples/4. PerformanceTests/Benchmark.cs
@@ -0,0 +1,83 @@
+namespace Advanced.Samples.Performance;
+
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Running;
+using FluentAssertions;
+using NUnit.Framework;
+
+[MemoryDiagnoser(true)]
+public class Benchmarks
+{
+ private const long num = long.MaxValue;
+
+ [Benchmark]
+ public void GetDigitsFromLeastSignificant_String()
+ {
+ num
+ .ToString()
+ .Select(x => Convert.ToByte(x.ToString()))
+ .ToArray();
+ }
+
+ [Benchmark]
+ public void GetDigitsFromLeastSignificant_MathWithSpan()
+ {
+ var result = new byte[20];
+ var span = new Span(result);
+ var n = num;
+ var index = 0;
+ while (n > 0)
+ {
+ span[index] = (byte)(n % 10);
+ n /= 10;
+ index++;
+ }
+
+ var res = span[..index].ToArray();
+ }
+
+ [Benchmark]
+ public void GetDigitsFromLeastSignificant_MathWithList()
+ {
+ var result = new List();
+ var n = num;
+ while (n > 0)
+ {
+ result.Add((byte)(n % 10));
+ n /= 10;
+ }
+ }
+
+ [Benchmark]
+ public void GetDigitsFromLeastSignificant_MathWithYield()
+ {
+ IEnumerable Inner()
+ {
+ var n = num;
+ while (n > 0)
+ {
+ yield return (byte)(n % 10);
+ n /= 10;
+ }
+ }
+
+ Inner().ToArray();
+ }
+}
+
+[TestFixture]
+[Explicit]
+public class BenchmarkTests
+{
+ [Test]
+ public void GetDigitsFromLeastSignificant()
+ {
+ var config = ManualConfig
+ .CreateMinimumViable()
+ .WithOption(ConfigOptions.DisableOptimizationsValidator, true);
+
+ BenchmarkRunner.Run(config);
+ }
+}
\ No newline at end of file
diff --git a/Testing/Advanced/slides.pptx b/Testing/Advanced/slides.pptx
new file mode 100644
index 0000000..511e954
Binary files /dev/null and b/Testing/Advanced/slides.pptx differ
diff --git a/Testing/Basic/Classwork/1. WordsStatistics/WordCount.cs b/Testing/Basic/Classwork/1. WordsStatistics/WordCount.cs
index a23e99e..57f4822 100644
--- a/Testing/Basic/Classwork/1. WordsStatistics/WordCount.cs
+++ b/Testing/Basic/Classwork/1. WordsStatistics/WordCount.cs
@@ -1,9 +1,15 @@
namespace Basic.Task.WordsStatistics.WordsStatistics;
-public struct WordCount(string word, int count)
+public struct WordCount
{
- public string Word { get; set; } = word;
- public int Count { get; set; } = count;
+ public WordCount(string word, int count)
+ {
+ Word = word;
+ Count = count;
+ }
+
+ public string Word { get; set; }
+ public int Count { get; set; }
public static WordCount Create(KeyValuePair pair)
{
diff --git a/Testing/Basic/Classwork/2. TDD/GameTests.cs b/Testing/Basic/Classwork/2. TDD/GameTests.cs
index ccedb2b..8e14ef9 100644
--- a/Testing/Basic/Classwork/2. TDD/GameTests.cs
+++ b/Testing/Basic/Classwork/2. TDD/GameTests.cs
@@ -4,9 +4,11 @@
namespace TDD;
+[TestFixture]
public class GameTests
{
[Test]
+ [Explicit]
public void HaveZeroScore_BeforeAnyRolls()
{
new Game()
diff --git a/Testing/Basic/Classwork/2. TDD/Readme.md b/Testing/Basic/Classwork/2. TDD/Readme.md
index db49fed..28840b4 100644
--- a/Testing/Basic/Classwork/2. TDD/Readme.md
+++ b/Testing/Basic/Classwork/2. TDD/Readme.md
@@ -1,10 +1,10 @@
-#
+# Задание
- . TDD
+Реализовать логику подсчета очков в игре Боулинг. Во время разработки придерживаться TDD
-1.
-2. ,
-3.
+1. Добавьте простейший красный тест
+2. Добавьте простейший код, проходящий тест
+3. Рефакторинг
1. ...
2. ...
@@ -12,16 +12,16 @@
...
-#
+# Правила игры
- 10 , , 10 . .
+Игра состоит из 10 фреймов, в каждом фрейме у игрока есть две попытки, чтобы выбить 10 кеглей. Счет за фрейм – это количество сбитых кеглей плюс бонусы за страйки и спэры.
- (spare) , 10 . , .
- 3 10 5.
+Спэр (spare) – это ситуация, когда игрок выбивает 10 кеглей двумя бросками. Бонус в этом фрейме равен количеству кеглей, сбитых следующим броском.
+Счет за 3 фрейм равен 10 плюс бонус в 5.
- (strike) , 10 . , .
- 5 10, 0 + 1.
+Страйк (strike) – это ситуация, когда игрок выбивает 10 кеглей первым броском. Бонус в этом фрейме равен количеству кеглей, сбитых следующими двумя бросками.
+Счет за 5 фрейм равен 10, плюс бонус в 0 + 1.
- , , , . 3. .
- 9 10 + 2 + 8. 10 2 + 8 + 6.
+В десятом фрейме игрок, выбивающий спэр или страйк, получает дополнительный бросок, чтобы закончить фрейм. Максимальное число бросков в десятом фрейме – 3. Бонусные очки в этом фрейме не начисляются.
+В 9 фрейме счет за фрейм равен 10 + 2 + 8. За 10 фрейм счет равен 2 + 8 + 6.
diff --git a/Testing/Basic/Homework/Readme.md b/Testing/Basic/Homework/Readme.md
index dbdbf85..7c7a585 100644
--- a/Testing/Basic/Homework/Readme.md
+++ b/Testing/Basic/Homework/Readme.md
@@ -1,25 +1,25 @@
-#
+# Домашнее задание
-##
+## Сравнение объектов
- ObjectComparison.
- [ FluentAssertions](http://fluentassertions.com/documentation.html).
+Изучите тест в классе ObjectComparison.
+Затем изучите [документацию FluentAssertions](http://fluentassertions.com/documentation.html).
- FluentAssertions :
+Перепишите тест с использованием наиболее подходящего метода FluentAssertions так чтобы:
-* ,
-* ,
-* : Person .
+* тест продолжал работать,
+* его читаемость возрасла,
+* он стал расширяем: добавление свойст в класс Person должно приводить к минимуму изменений в тестах.
- , CheckCurrentTsar_WithCustomEquality.
+В комментариях поясните, чем ваше решение лучше решения в методе CheckCurrentTsar_WithCustomEquality.
-##
+## Рефакторинг тестов
- NumberValidatorTests.
+Изучите код теста в классе NumberValidatorTests.
- ,
+Перепишите тест так, чтобы
-* ,
-* ,
-* - ,
-* .
+* найти и удалить повторяющиеся проверки,
+* найти недостающие проверки,
+* при падении теста было без стек-трейса понятно на каких данных код не работает,
+* одна упавшая проверка не блокировала прохождение остальных проверок.
diff --git a/Testing/Basic/Readme.md b/Testing/Basic/Readme.md
index 62d0379..78a17bb 100644
--- a/Testing/Basic/Readme.md
+++ b/Testing/Basic/Readme.md
@@ -1,44 +1,44 @@
-#
+# Тестирование
- .
+Это блок о написании правильных и полезных тестов.
- , :
+Пройдя блок, ты:
-- :
- - AAA
- - ,
-- , ,
-- " " " "
-- , , code review
--
-- ,
-- , TDD - :-)
-- TDD.
-- TDD
+- Узнаешь паттерны создания тестов:
+ - каноническую структуру теста AAA
+ - правила именования тестов, чтобы они работали как спецификация
+- Познакомишься с антипаттернами, которые приводят к хрупкости, сложности и трудночитаемости
+- Получишь опыт тестирования "черного ящика" и "белого ящика"
+- Поймешь, когда лучше работают тесты, а когда code review
+- Почувствуешь пользу от написания тестов
+- Узнаешь, почему полезно писать тесты вместе с кодом
+- Скорее всего поймешь, что никогда раньше не писал тесты в стиле TDD по-настоящему :-)
+- Получишь опыт парного TDD.
+- Станешь считать стиль TDD естественным и удобным в работе
-##
+## Необходимые знания
- C#
+Понадобится знание C#
-##
+## Самостоятельная подготовка
### C#
-1. NUnit, , nuget
-2. NUnit [](https://github.com/nunit/nunit-csharp-samples/blob/master/syntax/AssertSyntaxTests.cs) [](https://github.com/nunit/docs/wiki/NUnit-Documentation)
-3. Visual Studio Resharper [](https://www.jetbrains.com/resharper/features/unit_testing.html)
-4. [FluentAssertions](https://fluentassertions.com/introduction)
-5. .NET 8.
+1. Познакомься с NUnit, если ещё не знаком, научись подключать его к проекту через nuget
+2. Изучи возможности синтаксиса NUnit по этому [примеру](https://github.com/nunit/nunit-csharp-samples/blob/master/syntax/AssertSyntaxTests.cs) или по [документации](https://github.com/nunit/docs/wiki/NUnit-Documentation)
+3. Научись запускать тесты из Visual Studio с помощью Resharper по [инструкции](https://www.jetbrains.com/resharper/features/unit_testing.html)
+4. Изучи возможности синтаксиса [FluentAssertions](https://fluentassertions.com/introduction)
+5. Установи .NET 8.
-##
+## Очная встреча
-~ 3
+~ 3 часа
-##
+## Закрепление материала
-1. ____
- - . , ?
-2. __Test infection__
- ,
+1. Спецзадание __Ретротестирование__
+Вспомни одну-две решенные задачи. Какие тесты пригодились бы, если бы решение надо было дополнить или переписать?
+2. Спецзадание __Test infection__
+Решив задачу по программированию, напиши на нее модульные тесты
diff --git a/Testing/Basic/Samples/3. Antipatterns/StackTests.cs b/Testing/Basic/Samples/3. Antipatterns/StackTests.cs
index df1e27b..fc58dcc 100644
--- a/Testing/Basic/Samples/3. Antipatterns/StackTests.cs
+++ b/Testing/Basic/Samples/3. Antipatterns/StackTests.cs
@@ -1,12 +1,13 @@
using FluentAssertions;
using NUnit.Framework;
-namespace P1.Basic.Samples.Antipatterns;
+namespace Basic.Samples._3._Antipatterns;
[TestFixture]
-public class Stack1_Tests
+public class Stack1Tests
{
[Test]
+ [Explicit]
public void Test1()
{
var lines = File.ReadAllLines(@"C:\work\edu\testing-course\Patterns\bin\Debug\data.txt")
diff --git a/Testing/Testing.sln b/Testing/Testing.sln
new file mode 100644
index 0000000..0e07480
--- /dev/null
+++ b/Testing/Testing.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.10.35013.160
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Basic", "Basic\Basic.csproj", "{6ED454CB-A772-43E5-B72D-3FE9DA27337F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Advanced", "Advanced\Advanced.csproj", "{91372C0C-4DCD-44A0-ADEC-31BA2FD24568}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {6ED454CB-A772-43E5-B72D-3FE9DA27337F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6ED454CB-A772-43E5-B72D-3FE9DA27337F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6ED454CB-A772-43E5-B72D-3FE9DA27337F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6ED454CB-A772-43E5-B72D-3FE9DA27337F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {91372C0C-4DCD-44A0-ADEC-31BA2FD24568}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {91372C0C-4DCD-44A0-ADEC-31BA2FD24568}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {91372C0C-4DCD-44A0-ADEC-31BA2FD24568}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {91372C0C-4DCD-44A0-ADEC-31BA2FD24568}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {AB77118D-3CDB-4294-8843-84D376529C0D}
+ EndGlobalSection
+EndGlobal