Comments 7
Какой ужас... Даже не в плане чистоты кода, а именно подхода.
Если вам надо сделать обобщенный тест, который бы перебирал методы тестируемого класса, то это лучше делать через передачу функций как параметров, например так:
public class Example
{
public class Subject
{
public int Sum(int x, int y) => x + y;
public double Multiply(double x, double y) => x * y;
}
public static IEnumerable<object?[]> TestData
=> new[]
{
new object[] {(Subject s) => s.Sum(1, 2), 3},
new object[] {(Subject s) => s.Sum(0, 8), 8},
new object[] {(Subject s) => s.Multiply(1, 2), 2},
new object[] {(Subject s) => s.Multiply(8, 0), 0},
};
[Theory]
[MemberData(nameof(TestData))]
public void TestTheory<T>(Func<Subject, T> act, T expected)
{
var subject = new Subject();
var actual = act(subject);
Assert.Equal(expected, actual);
}
}
А когда вам для тестов нужно внедряться внутрь тестируемого класса, например тестировать приватные методы, то лучше отказаться от этой затеи вообще и переписать тестируемый класс так, чтобы его тестировать было удобно. Возможно, для этого потребуется разбить его на несколько классов и тестировать их отдельно.
По опыту знаю, что попытки пролезть тестами там, где неудобно, через рефлексию заканчиваются в лучшем случае хрупкими тестами, на поддержку которых уходит непростительно много времени.
Рефлексия же нужна в очень исключительных случаях, например в АОП, но никак не в тестах.
Если у вас данные приходят десериализованные из внешнего источника а тестируемые методы и типы являются универсальными, то ваш код уже не подходит, и вам также придется использовать рефлексию. Идея именно в этом заключалась. Смысл этой надстройки как раз в том чтобы упростить работу с универсальными типами и методами когда на входе "нетипизированые" данные.
По поводу тестирования закрытых членов я полностью с вами согласен, но...приходилось писать специальный тест на стороннюю сборку. Да и использование этой надстройки гораздо шире, чем тесты.
Смысл этой надстройки как раз в том чтобы упростить работу с универсальными типами и методами когда на входе "нетипизированые" данные
Не надо работать с нетипизированными данными, типизируйте их :) У типизации много преимуществ, надо ими пользоваться. Главное из них - нахождение ошибок еще до запуска тестов, на стадии компиляции.
Даже для данных из внешнего источника рефлексия не нужна, смотрите:
public class Example
{
public class Subject
{
public int Sum(int x, int y) => x + y;
public double Multiply(double x, double y) => x * y;
}
private abstract class Case {}
private class Case<T> : Case
{
public T Expected { get; init; }
}
private class SumCase : Case<int>
{
public int A { get; init; }
public int B { get; init; }
}
private class MultiplyCase : Case<double>
{
public double A { get; init; }
public double B { get; init; }
}
private static List<Case> ParseFile(string fileName)
{
...
}
public static IEnumerable<object?[]> TestData
=> ParseFile("test-cases.txt")
.Select(x => x switch {
SumCase sc => new object[] {(Subject s) => s.Sum(sc.A, sc.B), sc.Expected},
MultiplyCase mc => new object[] {(Subject s) => s.Multiply(mc.A, mc.B), mc.Expected}
});
[Xunit.Theory]
[MemberData(nameof(TestData))]
public void TestTheory<T>(Func<Subject, T> act, T expected)
{
var subject = new Subject();
var actual = act(subject);
Assert.Equal(expected, actual);
}
}
Так давайте уже придем к взаимопониманию...
Никто здесь не спорит что типизация это плохо и т.п. Тем более я и сам в своем случае начинал писать методы пытаясь типизировать объекты, но потом меня это занятие сильно утомило, т.к. писать тесты на несколько сотен универсальных функций да еще и с различными наборами параметров на каждую. Притом данные приходят из внешнего источника, да еще и в некоторых случаях могут ожидаться объекты любых типов и они гораздо сложнее чем те которые приводите вы. И вот вы сами же показали выше написанные адаптеры. А чем объемнее и сложнее код, тем выше вероятность сделать ошибку в тестах и отлаживать уже их. И загружались у меня из внешннего источника куда более сложные типы данных чем int и long, И сколько это бы заняло времени? Вот и был найден способ ускорить данную процедуру. Тем более что написав с десяток адаптеров, пришлось отлаживать уже их. Поэтому и было принято данное решение. Вам не требуется тратить время на написание различных типизирующих адаптеров. Здесь вам нужно лишь передать десериализованный объект сразу в функцию без каких-либо накладных расходов.
Еще раз скажу, что фактически здесь приводится инструмент позволяющий очень сильно упростить работу с унивирсальными типами и методами и "нетипизированными" данными которые надо обработать этими методами. И писалась данная надстройка конкретно не для использования в тестировании, но использование в конкретном кейсе тестирования приводится в качестве ПРИМЕРА. Здесь статья не про методологию тестирования, а про применение надстройки работы с отражением в конкретном примере тестирования. Вы видимо не так поняли посыл статьи, или я неправильно его раскрыл.
P.S. Да, вспомнил еще в чем была трудность. Типы объектов которые приходили на тестирование также являлись закрытыми универсальными типами с несколькми параметрами.
Появилось немного времени и дабы исключить последующие комментарии вышенаписанной тематики, дам более развернутое объяснение для чего это было сделано в тестировании. Скажу сразу, что я ни в коей мере не намерен оспаривать то что написал автор вышенаписанных комментариев, но это действительно касается именно типизированных данных. Здесь приведу пример, который должен удовлетворить подобных комментаторов.
Например, есть у нас такой код:
public class Subject<T1, T2>
{
}
public class Processor<T1>
{
public static R Method<M1, M2, R>(M1 m1, M2 m2)
where M1 : Subject<T1, M2>
{
}
}
public class B1 { }
public class B2 { }
public class C1 { }
public class C2 { }
И на вход тестовой функции поступают из внешенго источника "нетипизированные" приведенные к типу object экземпляры классов Subject<B1, C2>, Subject<B2, C1>, Subject<B2,C2>..., вернее массив параметров тестируемого метода Method класса Processor. Таким источником может быть как файл, база данных и в моем случае это также отлаживаемый генератор объектов, который может генерировать данные универсальных типов в runtime. Если использовать предлагаемую надстройку, то необходимо написать лишь один вызов.
[Xunit.Theory]
[MemberData(nameof(TestData))]
public void TestTheory(object[] inputs, object expected)
{
var actual = Reflector.CallMethod(typeof(Processor<>), MemberAccessibility.Public, null, null, inputs, null, null);
Assert.Equal(expected, actual);
}
Опечатка в коде вызова метода:
var actual = Reflector.CallMethod(typeof(Processor<>), "Method", MemberAccessibility.Public, null, null, inputs, null, null);
Здесь я опять допустил ошибку :). Вызов должен быть такой:
var actual = Reflector.CallMethod(typeof(Processor<>), "Method", MemberAccessibility.Public, null, null, inputs, null, expected.GetType());
Если мы допускаем null-объекты в типе R, то необходимо ожидаемый тип передавать так же в параметрах, иначе можно в ограничении аргументов метода для типа R поставить notnull. А так все должно работать, проверил.
.NET Reflection. Упрощаем работу и используем в тестировании