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