Skip to content

Commit

Permalink
refactored tests
Browse files Browse the repository at this point in the history
  • Loading branch information
SenyaPevko committed Nov 26, 2023
1 parent b6b4fd4 commit da2fc5c
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 132 deletions.
2 changes: 1 addition & 1 deletion cs/HomeExercises/HomeExercises.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<PackageReference Include="JetBrains.Annotations" Version="2020.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>

</Project>
186 changes: 123 additions & 63 deletions cs/HomeExercises/NumberValidatorTests.cs
Original file line number Diff line number Diff line change
@@ -1,80 +1,140 @@
using System;
using System.Text.RegularExpressions;
using FluentAssertions;
using FluentAssertions.Execution;
using static FluentAssertions.FluentActions;
using NUnit.Framework;
using System.Collections;

namespace HomeExercises
{
public class NumberValidatorTests
{
[Test]
public void Test()
{
Assert.Throws<ArgumentException>(() => new NumberValidator(-1, 2, true));
Assert.DoesNotThrow(() => new NumberValidator(1, 0, true));
Assert.Throws<ArgumentException>(() => new NumberValidator(-1, 2, false));
Assert.DoesNotThrow(() => new NumberValidator(1, 0, true));
[TestFixture]
public class NumberValidatorTests
{
public static IEnumerable IsValidNumberPrecisionTests
{
get
{
yield return new TestCaseData(3, 2, true, "+0.00").Returns(false)
.SetName("IsValidNumber_ReturnsFalse_WhenSymbolWithNumberLengthGreaterThanPrecision");
yield return new TestCaseData(3, 2, true, "00.00").Returns(false)
.SetName("IsValidNumber_ReturnsFalse_WhenIntPartWithFracPartGreaterThanPrecision");
yield return new TestCaseData(17, 2, true, "0").Returns(true)
.SetName("IsValidNumber_ReturnsTrue_WhenNumberLengthNotGreaterThanPrecision");
yield return new TestCaseData(4, 2, true, "+1.23").Returns(true)
.SetName("IsValidNumber_ReturnsTrue_WhenPositiveSymbolWithNumberLengthNotGreaterThanPrecision");
yield return new TestCaseData(4, 2, false, "-1.23").Returns(true)
.SetName("IsValidNumber_ReturnsTrue_WhenNegativeSymbolWithNumberLengthNotGreaterThanPrecision");
}
}
public static IEnumerable IsValidNumberScaleTests
{
get
{
yield return new TestCaseData(17, 2, true, "0.000").Returns(false)
.SetName("IsValidNumber_ReturnsFalse_WhenFracPartGreaterThanScale");
yield return new TestCaseData(17, 2, true, "0.0").Returns(true)
.SetName("IsValidNumber_ReturnsTrue_WhenFracPartNotGreaterThanScale");
}
}
public static IEnumerable IsValidNumberPositivityTests
{
get
{
yield return new TestCaseData(3, 2, true, "-0.00").Returns(false)
.SetName("IsValidNumber_ReturnsFalse_WhenAcceptsOnlyPositiveButGivenNegativeNumber");
yield return new TestCaseData(3, 2, false, "-0.0").Returns(true)
.SetName("IsValidNumber_ReturnsTrue_WhenAcceptsAnyAndGivenNegativeNumber");
}
}
public static IEnumerable IsValidNumberSymbolsTests
{
get
{
yield return new TestCaseData(3, 2, true, "a.sd").Returns(false)
.SetName("IsValidNumber_ReturnsFalse_WhenGivenNotDigits");
yield return new TestCaseData(17, 2, true, "").Returns(false)
.SetName("IsValidNumber_ReturnsFalse_WhenEmptyStringGiven");
}
}

Assert.IsTrue(new NumberValidator(17, 2, true).IsValidNumber("0.0"));
Assert.IsTrue(new NumberValidator(17, 2, true).IsValidNumber("0"));
Assert.IsTrue(new NumberValidator(17, 2, true).IsValidNumber("0.0"));
Assert.IsFalse(new NumberValidator(3, 2, true).IsValidNumber("00.00"));
Assert.IsFalse(new NumberValidator(3, 2, true).IsValidNumber("-0.00"));
Assert.IsTrue(new NumberValidator(17, 2, true).IsValidNumber("0.0"));
Assert.IsFalse(new NumberValidator(3, 2, true).IsValidNumber("+0.00"));
Assert.IsTrue(new NumberValidator(4, 2, true).IsValidNumber("+1.23"));
Assert.IsFalse(new NumberValidator(3, 2, true).IsValidNumber("+1.23"));
Assert.IsFalse(new NumberValidator(17, 2, true).IsValidNumber("0.000"));
Assert.IsFalse(new NumberValidator(3, 2, true).IsValidNumber("-1.23"));
Assert.IsFalse(new NumberValidator(3, 2, true).IsValidNumber("a.sd"));
}
}

public class NumberValidator
{
private readonly Regex numberRegex;
private readonly bool onlyPositive;
private readonly int precision;
private readonly int scale;
public static IEnumerable ConstructorArgumentExceptions
{
get
{
yield return new TestCaseData(-1,2,true)
.SetName("Constructor_ThrowsArgumentExceptionWhenPercisionNotPositive");
yield return new TestCaseData(1,2,true)
.SetName("Constructor_ThrowsArgumentExceptionWhenScaleGreaterThanPercision");
yield return new TestCaseData(1,-1,true)
.SetName("Constructor_ThrowsArgumentExceptionWhenScaleNotPositive");
yield return new TestCaseData(1,1,true)
.SetName("Constructor_ThrowsArgumentExceptionWhenScaleEqualsPercision");
}
}

public NumberValidator(int precision, int scale = 0, bool onlyPositive = false)
{
this.precision = precision;
this.scale = scale;
this.onlyPositive = onlyPositive;
if (precision <= 0)
throw new ArgumentException("precision must be a positive number");
if (scale < 0 || scale >= precision)
throw new ArgumentException("precision must be a non-negative number less or equal than precision");
numberRegex = new Regex(@"^([+-]?)(\d+)([.,](\d+))?$", RegexOptions.IgnoreCase);
}
[Test, TestCaseSource(nameof(IsValidNumberPositivityTests)),
TestCaseSource(nameof(IsValidNumberPrecisionTests)),
TestCaseSource(nameof(IsValidNumberScaleTests)),
TestCaseSource(nameof(IsValidNumberSymbolsTests))]
public bool IsValidNumber_Returns(int precision, int scale, bool onlyPositive, string number)
{
return new NumberValidator(precision, scale, onlyPositive).IsValidNumber(number);
}

public bool IsValidNumber(string value)
{
// Проверяем соответствие входного значения формату N(m,k), в соответствии с правилом,
// описанным в Формате описи документов, направляемых в налоговый орган в электронном виде по телекоммуникационным каналам связи:
// Формат числового значения указывается в виде N(m.к), где m – максимальное количество знаков в числе, включая знак (для отрицательного числа),
// целую и дробную часть числа без разделяющей десятичной точки, k – максимальное число знаков дробной части числа.
// Если число знаков дробной части числа равно 0 (т.е. число целое), то формат числового значения имеет вид N(m).
[Test, TestCaseSource(nameof(ConstructorArgumentExceptions))]
public void Constructor_ThrowsArgumentException(int precision, int scale, bool onlyPositive)
{
Invoking(() => new NumberValidator(precision, scale, onlyPositive)).Should().Throw<ArgumentException>();
}
}

if (string.IsNullOrEmpty(value))
return false;
public class NumberValidator
{
private readonly Regex numberRegex;
private readonly bool onlyPositive;
private readonly int precision;
private readonly int scale;

var match = numberRegex.Match(value);
if (!match.Success)
return false;
public NumberValidator(int precision, int scale = 0, bool onlyPositive = false)
{
this.precision = precision;
this.scale = scale;
this.onlyPositive = onlyPositive;
if (precision <= 0)
throw new ArgumentException("precision must be a positive number");
if (scale < 0 || scale >= precision)
throw new ArgumentException("precision must be a non-negative number less or equal than precision");
numberRegex = new Regex(@"^([+-]?)(\d+)([.,](\d+))?$", RegexOptions.IgnoreCase);
}

// Знак и целая часть
var intPart = match.Groups[1].Value.Length + match.Groups[2].Value.Length;
// Дробная часть
var fracPart = match.Groups[4].Value.Length;
public bool IsValidNumber(string value)
{
// Проверяем соответствие входного значения формату N(m,k), в соответствии с правилом,
// описанным в Формате описи документов, направляемых в налоговый орган в электронном виде по телекоммуникационным каналам связи:
// Формат числового значения указывается в виде N(m.к), где m – максимальное количество знаков в числе, включая знак (для отрицательного числа),
// целую и дробную часть числа без разделяющей десятичной точки, k – максимальное число знаков дробной части числа.
// Если число знаков дробной части числа равно 0 (т.е. число целое), то формат числового значения имеет вид N(m).

if (intPart + fracPart > precision || fracPart > scale)
return false;
if (string.IsNullOrEmpty(value))
return false;

if (onlyPositive && match.Groups[1].Value == "-")
return false;
return true;
}
}
var match = numberRegex.Match(value);
if (!match.Success)
return false;

// Знак и целая часть
var intPart = match.Groups[1].Value.Length + match.Groups[2].Value.Length;
// Дробная часть
var fracPart = match.Groups[4].Value.Length;

if (intPart + fracPart > precision || fracPart > scale)
return false;

if (onlyPositive && match.Groups[1].Value == "-")
return false;
return true;
}
}
}
154 changes: 86 additions & 68 deletions cs/HomeExercises/ObjectComparison.cs
Original file line number Diff line number Diff line change
@@ -1,83 +1,101 @@
using FluentAssertions;
using NUnit.Framework;
using System.Net;

namespace HomeExercises
{
public class ObjectComparison
{
[Test]
[Description("Проверка текущего царя")]
[Category("ToRefactor")]
public void CheckCurrentTsar()
{
var actualTsar = TsarRegistry.GetCurrentTsar();
public class ObjectComparison
{
[Test]
[Description("Проверка текущего царя")]
[Category("ToRefactor")]
public void CheckCurrentTsar()
{
var actualTsar = TsarRegistry.GetCurrentTsar();

var expectedTsar = new Person("Ivan IV The Terrible", 54, 170, 70,
new Person("Vasili III of Russia", 28, 170, 60, null));
var expectedTsar = new Person("Ivan IV The Terrible", 54, 170, 70,
new Person("Vasili III of Russia", 28, 170, 60, null));

// Перепишите код на использование Fluent Assertions.
Assert.AreEqual(actualTsar.Name, expectedTsar.Name);
Assert.AreEqual(actualTsar.Age, expectedTsar.Age);
Assert.AreEqual(actualTsar.Height, expectedTsar.Height);
Assert.AreEqual(actualTsar.Weight, expectedTsar.Weight);
// сравнение родителей такое же как и в коде, который нужно было отрефакторить - сравнивает по ссылке,
// а значит при создании экземпляра для сравнения, если родитель родителя будет не null,
// то будет ошибка, если это не ошибка в тесте, а так и должно быть,
// то реализация CheckCurrentTsar_WithCustomEquality работает неправильно,
// тк в таком случае она засчитает тест
// также данная реализация CheckCurrentTsar не сравнивает поле Weight для родителя,
// также как и оригинальный код, из-за чего данная реализация
// будет показывать результат отличный от CheckCurrentTsar_WithCustomEquality при
// небольших изменениях в TsarRegistry или в экземпляре expectedTsar

Assert.AreEqual(expectedTsar.Parent!.Name, actualTsar.Parent!.Name);
Assert.AreEqual(expectedTsar.Parent.Age, actualTsar.Parent.Age);
Assert.AreEqual(expectedTsar.Parent.Height, actualTsar.Parent.Height);
Assert.AreEqual(expectedTsar.Parent.Parent, actualTsar.Parent.Parent);
}
actualTsar.Should().BeEquivalentTo(expectedTsar, options => options
.Excluding(tsar => tsar.Id)
.Excluding(tsar => tsar.Parent));
expectedTsar.Parent.Should().BeEquivalentTo(actualTsar.Parent!, options => options
.Excluding(parent => parent.Id)
.Excluding(parent => parent.Weight));

[Test]
[Description("Альтернативное решение. Какие у него недостатки?")]
public void CheckCurrentTsar_WithCustomEquality()
{
var actualTsar = TsarRegistry.GetCurrentTsar();
var expectedTsar = new Person("Ivan IV The Terrible", 54, 170, 70,
new Person("Vasili III of Russia", 28, 170, 60, null));
// данная реализация лучше CheckCurrentTsar_WithCustomEquality тем, что:
// 1) при ошибке в тесте, выводится объяснение того, что пошло не так
// 2) код теста легко читается - методы записаны последовательно, и что они делают понятно из названия
// 3) название теста на прямую отражает происходящее в коде и соответствует выводимому результату
// 4) более удобная в плане расширяемости

// Какие недостатки у такого подхода?
Assert.True(AreEqual(actualTsar, expectedTsar));
}
}

private bool AreEqual(Person? actual, Person? expected)
{
if (actual == expected) return true;
if (actual == null || expected == null) return false;
return
actual.Name == expected.Name
&& actual.Age == expected.Age
&& actual.Height == expected.Height
&& actual.Weight == expected.Weight
&& AreEqual(actual.Parent, expected.Parent);
}
}
[Test]
[Description("Альтернативное решение. Какие у него недостатки?")]
public void CheckCurrentTsar_WithCustomEquality()
{
var actualTsar = TsarRegistry.GetCurrentTsar();
var expectedTsar = new Person("Ivan IV The Terrible", 54, 170, 70,
new Person("Vasili III of Russia", 28, 170, 60, null));

public class TsarRegistry
{
public static Person GetCurrentTsar()
{
return new Person(
"Ivan IV The Terrible", 54, 170, 70,
new Person("Vasili III of Russia", 28, 170, 60, null));
}
}
// Какие недостатки у такого подхода?
// 1) функциональность теста разбита на 2 метода, усложняя его читаемость
// 2) тест уже посути не сравнивает царей, а проверяет работу метода AreEqual
// 3) отсутствие пояснения - только выкинет "ожидается true, а было false" при ошибке
// 4) такая реализация AreEqual может вызвать переполнение стэка, тк сравнение родителей рекурсивно
Assert.True(AreEqual(actualTsar, expectedTsar));
}

public class Person
{
public static int IdCounter = 0;
public int Age, Height, Weight;
public string Name;
public Person? Parent;
public int Id;
private bool AreEqual(Person? actual, Person? expected)
{
if (actual == expected) return true;
if (actual == null || expected == null) return false;
return
actual.Name == expected.Name
&& actual.Age == expected.Age
&& actual.Height == expected.Height
&& actual.Weight == expected.Weight
&& AreEqual(actual.Parent, expected.Parent);
}
}

public Person(string name, int age, int height, int weight, Person? parent)
{
Id = IdCounter++;
Name = name;
Age = age;
Height = height;
Weight = weight;
Parent = parent;
}
}
public class TsarRegistry
{
public static Person GetCurrentTsar()
{
return new Person(
"Ivan IV The Terrible", 54, 170, 70,
new Person("Vasili III of Russia", 28, 170, 60, null));
}
}

public class Person
{
public static int IdCounter = 0;
public int Age, Height, Weight;
public string Name;
public Person? Parent;
public int Id;

public Person(string name, int age, int height, int weight, Person? parent)
{
Id = IdCounter++;
Name = name;
Age = age;
Height = height;
Weight = weight;
Parent = parent;
}
}
}

0 comments on commit da2fc5c

Please sign in to comment.