Модульный тест C# для метода, который вызывает Console.ReadLine()
Я хочу создать модульный тест для функции-члена класса с именем ScoreBoard
который хранит пять лучших игроков в игре.
Проблема в том, что метод, для которого я создал тест (SignInScoreBoard
) звонит Console.ReadLine()
поэтому пользователь может ввести свое имя:
public void SignInScoreBoard(int steps)
{
if (topScored.Count < 5)
{
Console.Write(ASK_FOR_NAME_MESSAGE);
string name = Console.ReadLine();
KeyValuePair<string, int> pair = new KeyValuePair<string, int>(name, steps);
topScored.Insert(topScored.Count, pair);
}
else
{
if (steps < topScored[4].Value)
{
topScored.RemoveAt(4);
Console.Write(ASK_FOR_NAME_MESSAGE);
string name = Console.ReadLine();
topScored.Insert(4, new KeyValuePair<string, int>(name, steps));
}
}
}
Есть ли способ вставить как десять пользователей, чтобы я мог проверить, хранятся ли пять с меньшим количеством ходов (шагов)?
10 ответов
Вам нужно будет реорганизовать строки кода, которые вызывают Console.ReadLine, в отдельный объект, чтобы вы могли заглушить его собственной реализацией в своих тестах.
В качестве быстрого примера, вы можете просто сделать класс так:
public class ConsoleNameRetriever {
public virtual string GetNextName()
{
return Console.ReadLine();
}
}
Затем в вашем методе выполните рефакторинг, чтобы получить экземпляр этого класса. Однако во время теста вы можете переопределить это с помощью тестовой реализации:
public class TestNameRetriever : ConsoleNameRetriever {
// This should give you the idea...
private string[] names = new string[] { "Foo", "Foo2", ... };
private int index = 0;
public override string GetNextName()
{
return names[index++];
}
}
При тестировании поменяйте местами реализацию с тестовой реализацией.
Конечно, я лично использовал бы фреймворк, чтобы упростить это, и использовал чистый интерфейс вместо этих реализаций, но, надеюсь, вышесказанного достаточно, чтобы дать вам правильную идею...
Вы должны реорганизовать свой код, чтобы удалить зависимость от консоли из этого кода.
Например, вы можете сделать это:
public interface IConsole
{
void Write(string message);
void WriteLine(string message);
string ReadLine();
}
а затем измените свой код следующим образом:
public void SignInScoreBoard(int steps, IConsole console)
{
... just replace all references to Console with console
}
Чтобы запустить его в производство, передайте ему экземпляр этого класса:
public class ConsoleWrapper : IConsole
{
public void Write(string message)
{
Console.Write(message);
}
public void WriteLine(string message)
{
Console.WriteLine(message);
}
public string ReadLine()
{
return Console.ReadLine();
}
}
Однако во время теста используйте это:
public class ConsoleWrapper : IConsole
{
public List<String> LinesToRead = new List<String>();
public void Write(string message)
{
}
public void WriteLine(string message)
{
}
public string ReadLine()
{
string result = LinesToRead[0];
LinesToRead.RemoveAt(0);
return result;
}
}
Это делает ваш код проще для тестирования.
Конечно, если вы хотите проверить, что записан правильный вывод, вам нужно добавить код в методы записи, чтобы собрать вывод, чтобы вы могли утверждать его в своем тестовом коде.
Вы не должны издеваться над чем-то, что исходит от фреймворка, .NET уже предоставляет абстракции для своих компонентов. Для консоли это методы Console.SetIn() и Console.SetOut().
Например, в отношении Console.Readline() вы должны сделать это следующим образом:
[TestMethod]
MyTestMethod()
{
Console.SetIn(new StringReader("fakeInput"));
var result = MyTestedMethod();
StringAssert.Equals("fakeInput", result);
}
Учитывая, что проверенный метод возвращает ввод, прочитанный Console.Readline(). Метод будет использовать строку, которую мы установили в качестве ввода для консоли, и не будет ждать интерактивного ввода.
Вы можете использовать родинки, чтобы заменить Console.ReadLine
с вашим собственным методом без необходимости вообще менять код (разработка и реализация абстрактной консоли с поддержкой внедрения зависимостей совершенно не нужны).
public void SignInScoreBoard(int steps, Func<String> nameProvider)
{
...
string name = nameProvider();
...
}
В вашем тестовом случае вы можете назвать его как
SignInScoreBoard(val, () => "TestName");
В вашей обычной реализации, назовите это как
SignInScoreBoard(val, Console.ReadLine);
Если вы используете C# 4.0, вы можете сделать Console.ReadLine значением по умолчанию, сказав
public void SignInScoreBoard(int steps, Func<String> nameProvider=null)
{
nameProvider = nameProvider ?? Console.ReadLine;
...
Почему бы не создать новый поток (файл / память) для stdin и stdout, а затем перенаправить ввод / вывод в новые потоки перед вызовом метода? После этого вы можете проверить содержимое потоков после завершения метода.
Вместо того, чтобы абстрагировать консоль, я бы скорее создал компонент для инкапсуляции этой логики, протестировал этот компонент и использовал его в консольном приложении.
Я не могу поверить, сколько людей ответили, не смотря на вопрос должным образом. Проблема в том, что рассматриваемый метод делает больше, чем одну вещь, то есть запрашивает имя и вставляет лучший результат. Любая ссылка на консоль может быть извлечена из этого метода, и вместо нее должно быть передано имя:
public void SignInScoreBoard(int steps, string nameOfTopScorer)
Для других тестов вы, вероятно, захотите абстрагировать чтение вывода консоли, как это предлагается в других ответах.
У меня была похожая проблема несколько дней назад. Класс Encapsulation Console казался мне излишним. Основываясь на принципах KISS и IoC/DI, я поместил зависимости для записи (вывод) и чтения (ввод) в конструктор. Позвольте мне показать пример.
Мы можем предположить, что простой поставщик подтверждения определяется интерфейсом IConfirmationProvider
public interface IConfirmationProvider
{
bool Confirm(string operation);
}
и его реализация
public class ConfirmationProvider : IConfirmationProvider
{
private readonly TextReader input;
private readonly TextWriter output;
public ConfirmationProvider() : this(Console.In, Console.Out)
{
}
public ConfirmationProvider(TextReader input, TextWriter output)
{
this.input = input;
this.output = output;
}
public bool Confirm(string operation)
{
output.WriteLine($"Confirmed operation {operation}...");
if (input.ReadLine().Trim().ToLower() != "y")
{
output.WriteLine("Aborted!");
return false;
}
output.WriteLine("Confirmated!");
return true;
}
}
Теперь вы можете легко протестировать свою реализацию, когда внедряете зависимость от вашего TextWriter
а также TextReader
(в этом примере StreamReader
как TextReader
)
[Test()]
public void Confirm_Yes_Test()
{
var cp = new ConfirmationProvider(new StringReader("y"), Console.Out);
Assert.IsTrue(cp.Confirm("operation"));
}
[Test()]
public void Confirm_No_Test()
{
var cp = new ConfirmationProvider(new StringReader("n"), Console.Out);
Assert.IsFalse(cp.Confirm("operation"));
}
И использовать вашу реализацию из стандартного способа применения с настройками по умолчанию (Console.In
как TextReader
а также Console.Out
как TextWriter
)
IConfirmationProvider cp = new ConfirmationProvider();
Вот и все - один дополнительный ctor с инициализацией полей.
ConsoleTests.cs (прохождение теста)
using NUnit.Framework;
class ConsoleTests
{
[Test]
public void TestsStdIn()
{
var playerName = "John Doe";
var capturedStdOut = CapturedStdOut(() =>
{
SubstituteStdIn(playerName, () =>
{
RunApp();
});
});
Assert.AreEqual($"Hello, {playerName}", capturedStdOut);
}
void RunApp(string[]? arguments = default)
{
var entryPoint = typeof(Program).Assembly.EntryPoint!;
entryPoint.Invoke(null, new object[] { arguments ?? Array.Empty<string>() });
}
string CapturedStdOut(Action callback)
{
TextWriter originalStdOut = Console.Out;
using var newStdOut = new StringWriter();
Console.SetOut(newStdOut);
callback.Invoke();
var capturedOutput = newStdOut.ToString();
Console.SetOut(originalStdOut);
return capturedOutput;
}
void SubstituteStdIn(string content, Action callback)
{
TextReader originalStdIn = Console.In;
using var newStdIn = new StringReader(content);
Console.SetIn(newStdIn);
callback.Invoke();
Console.SetIn(originalStdIn);
}
}
Program.cs (реализация)
Console.Write($"Hello, {Console.ReadLine()}");
PS Предложенные выше решения класса провайдера, ретривера и оболочки не решают проблему, описанную автором, поскольку вам в любом случае нужно проверить, действительно ли вы запрашиваете пользовательский ввод или печатаете что-то на стандартный вывод.