Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Яценко Ирина #235

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
154 changes: 90 additions & 64 deletions cs/HomeExercises/NumberValidatorTests.cs
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Круто, что коммиты логически разбиты

Original file line number Diff line number Diff line change
@@ -1,80 +1,106 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using FluentAssertions;
using NUnit.Framework;

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]
[TestFixture(TestOf = typeof(NumberValidator))]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[TestFixture] дублировать не надо, хватит одной записи - первой или второй (на мой взгляд более предпочтительной)

public class NumberValidatorTests
{
private static TestCaseData[] ArgumentExceptionTestCases =
{
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Очевидно, что слетело форматирование. Если пользуешься rider, то в нем можно прожать ctrl + alt + L / ctrl + alt + shift + L, код отформатируется. Можно так же настроить автоформатирование (можешь погуглить) на каждое сохранение кода ctrl + s - очень удобно, кстати, рекомендую. Если в vs работаешь, то не подскажу, но всё легко гуглится.

new TestCaseData(-1, 2, true).SetName("WhenPassNegativePrecision"),
new TestCaseData(1, -2, true).SetName("WhenPassNegativeScale"),
new TestCaseData(2, 2, true).SetName("WhenPrecisionIsEqualToTheScale")
};

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"));
}
}
[TestCaseSource(nameof(ArgumentExceptionTestCases))]
public void NumberValidatorCtor_WhenPassInvalidArguments_ShouldThrowArgumentException(int precision,
int scale, bool onlyPositive) =>
Assert.Throws<ArgumentException>(() => new NumberValidator(precision, scale, onlyPositive));

public class NumberValidator
{
private readonly Regex numberRegex;
private readonly bool onlyPositive;
private readonly int precision;
private readonly int scale;
[Test]
public void NumberValidatorCtor_WhenPassValidArguments_ShouldNotThrows() =>
Assert.DoesNotThrow(() => new NumberValidator(1, 0, true));

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);
}
private static TestCaseData[] ArgumentTestCases =
{
new TestCaseData(3,2,true,"a.sd", false).SetName("WhenLettersInsteadOfNumber"),
new TestCaseData(3,2,true,"2.!", false).SetName("WhenSymbolsInsteadOfNumber"),
new TestCaseData(3,2,true,"2,3", true).SetName("WhenCharactersAreSeparatedByComma"),
new TestCaseData(3,2,true,null!, false).SetName("WhenPassNumberIsNull"),
new TestCaseData(3,2,true,"2,.3", false).SetName("WhenTwoSeparatorsArePassed"),
new TestCaseData(3,2,true,"2 3", false).SetName("WhenSeparatedBySpace"),
new TestCaseData(3,2,true,"", false).SetName("WhenPassNumberIsEmpty"),
new TestCaseData(3,2,true,"-0.00", false).SetName("WhenIntPartWithNegativeSignMoreThanPrecision"),
new TestCaseData(3,2,true,"+1.23", false).SetName("WhenIntPartWithPositiveSignMoreThanPrecision"),
new TestCaseData(3,2,true,"0.000", false).SetName("WhenFractionalPartMoreThanScale"),
new TestCaseData(3,2,true,"0", true).SetName("WhenFractionalPartIsMissing"),
new TestCaseData(3,2,true,"0.0", true).SetName("WhenNumberIsValid")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new TestCaseData(3,2,true,"0.0", true).Returns().SetName("WhenNumberIsValid")

А bool expectedResult можешь убрать. Соответственно, тебе придется возвращать результат вызова IsValidNumber.

Copy link

@pakapik pakapik Nov 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Давай добавим чуть больше данных для проверок - null, спец.символы - /r , /n, просто символы %$# и т.д. Число с каким-то символом? Символ с каким-то числом? Строка из пробелов?

Что будет, если разделитель не точка, а запятая? А если разделителей несколько? А если в строке только разделитель?

Как валидатор ведет себя с очень большими числами? С очень большой точностью? А все вместе? Умеет ли он понимать числа в экспоненциальном формате?

А может, валидатор считает верным число в другой системе счисления? Иррациональные числа?

Возвращает ли валидатор на один и тот же входной параметр один и тот же результат? Для этого можно использовать атрибут [Repeat()].

А может, валидатор как-то меняет свой стейт? Что будет если подать сначала одно число, затем второе, а затем снова первое? Кстати, для того, чтоб обозначить, что метод ничего не изменяет (что-то внутри себя / входные параметры), то его можно пометить атрибутом [Pure] из неймспейса System.Diagnostics.Contracts или JetBrains.Annotations. В данном случае [Pure] применяться должен к IsValidNumber, раз он внутри себя не делает ничего такого криминального.

Тесты как документация кода должны отвечать на все эти вопросы и даже больше) Так что сама тоже подумай над тем, каких бы ещё тестов добавить

};

public bool IsValidNumber(string value)
{
// Проверяем соответствие входного значения формату N(m,k), в соответствии с правилом,
// описанным в Формате описи документов, направляемых в налоговый орган в электронном виде по телекоммуникационным каналам связи:
// Формат числового значения указывается в виде N(m.к), где m – максимальное количество знаков в числе, включая знак (для отрицательного числа),
// целую и дробную часть числа без разделяющей десятичной точки, k – максимальное число знаков дробной части числа.
// Если число знаков дробной части числа равно 0 (т.е. число целое), то формат числового значения имеет вид N(m).
private NumberValidator GetCorrectNumberValidator(int precision, int scale, bool onlyPositive) =>
new NumberValidator(precision, scale, onlyPositive);

if (string.IsNullOrEmpty(value))
return false;
[TestOf(nameof(NumberValidator.IsValidNumber))]
[TestCaseSource(nameof(ArgumentTestCases))]
public void WhenPassInvalidArguments_ShouldReturnFalse(int precision, int scale,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Все ещё неверное название теста. У тебя в источнике есть кейсы, которые проверяют, что IsValidNumber вернет true + туда подаются валидные аргументы.

bool onlyPositive, string number, bool expectedResult)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: очень странный перенос аргументов. Мб, у тебя по ширине строки это форматируется, не знаю. Обычно пишут либо в одну строку:
(int precision, int scale, bool onlyPositive, string number, bool expectedResult),
либо переносят вообще всё:

 (
int precision, 
int scale, 
bool onlyPositive, 
string number,
bool expectedResult)

{
var correctValidator = GetCorrectNumberValidator(precision, scale, onlyPositive);

var match = numberRegex.Match(value);
if (!match.Success)
return false;
Assert.AreEqual(expectedResult, correctValidator.IsValidNumber(number));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Не надо смешивать секцию Act и Assert. Отдельно напиши в var actualResult = correctValidator.IsValidNumber(number);, затем отдельно Assert.AreEqual(expectedResult, actualResult). Впрочем, когда ты переделаешь с учетом замечаний сверху, тебе вообще не понадобится писать Assert.

Но совет про не смешивать всё ещё актуальный (во всех смыслах🙃🙃🙃)

}
}

// Знак и целая часть
var intPart = match.Groups[1].Value.Length + match.Groups[2].Value.Length;
// Дробная часть
var fracPart = match.Groups[4].Value.Length;
public class NumberValidator
{
private readonly Regex numberRegex;
private readonly bool onlyPositive;
private readonly int precision;
private readonly int scale;

if (intPart + fracPart > precision || fracPart > scale)
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);
}

if (onlyPositive && match.Groups[1].Value == "-")
return false;
return true;
}
}
public bool IsValidNumber(string value)
{
// Проверяем соответствие входного значения формату N(m,k), в соответствии с правилом,
// описанным в Формате описи документов, направляемых в налоговый орган в электронном виде по телекоммуникационным каналам связи:
// Формат числового значения указывается в виде N(m.к), где m – максимальное количество знаков в числе, включая знак (для отрицательного числа),
// целую и дробную часть числа без разделяющей десятичной точки, k – максимальное число знаков дробной части числа.
// Если число знаков дробной части числа равно 0 (т.е. число целое), то формат числового значения имеет вид N(m).

if (string.IsNullOrEmpty(value))
return false;

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;
}
}
}
139 changes: 70 additions & 69 deletions cs/HomeExercises/ObjectComparison.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,81 +3,82 @@

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);
actualTsar.Should()
.BeEquivalentTo(expectedTsar, x => x
.Excluding(x => x.Id)
.Excluding(x => x.Parent!.Id));
}

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);
}
[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));

[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));
// Какие недостатки у такого подхода?
// Данный подход делает класс труднорасширяемым, ведь при добавлении новых полей в класс Person
// нужно переписывать и метод сравнения для всех этих полей, так же новые поля cможет добавить
// другой разработчик, который не знает о таком методе сравнения, что приведет к новым, неотловоенным ошибкам -
// новые поля не будут сравниваться.
// В моем решении (CheckCurrentTsar), такой ошибки не возникнет и класс Person сможет без ошибок расширяться
// при условии, что сравниваться будут все поля, кроме Id (так же и их Parent), так как код написан не перебиранием
// всех полей для сравнения, а сравнением объекта в целом с исключением его Id.
Assert.True(AreEqual(actualTsar, expectedTsar));
}

// Какие недостатки у такого подхода?
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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ранее не обратил внимание, но объясни ещё, пж, про 52 строку - в чем тут потенциальная опасность?

}
}

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 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 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 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;
}
}
public Person(string name, int age, int height, int weight, Person? parent)
{
Id = IdCounter++;
Name = name;
Age = age;
Height = height;
Weight = weight;
Parent = parent;
}
}
}