Тестирование ввода пользователя, и обработка ошибок

Декомпозируем проверку ввода

Допустим я хочу запросить у пользователя размер матрицы и ввести ее элементы. Сделать это можно так:

static void Main(string[] args)
{
    // заведем переменную под размер матрицы
    int matrixSize;
    while (true) // используем бесконечный цикл чтобы давать пользователю вводить данные пока ответ не станет корректным
    {
        var input = Console.ReadLine(); // запрашиваем ввод у пользователя

        // сначала проверяем ввели ли число
        if (!int.TryParse(inputLine, out matrixSize))
        {
            Console.WriteLine("Вы ввели не число");
            continue; // если не число, то вызываем continue чтобы снова отправить юзера на запрос нового значения
        }
        // теперь проверяем размерность
        if (!(matrixSize > 0 && matrixSize < 10))
        {
            Console.WriteLine("Число должно быть в переделах от 0 до 10");
            continue; // если не в пределах вызываем continue чтобы снова отправить юзера на запрос нового значения
        }

        // если мы попали сюда, значит все ок и можно выйти из цикла
        break;
    }

    Console.ReadKey();
}

как теперь сделать этот код тестируемым?

Для этого нам надо вынести проверки в отдельную функцию. Однако есть проблема, внутри проверок у нас есть ключевое слово continue, которое строго привязано к циклу и может начать нам мешать. Как с этим бороться пока не понятно.

Давайте сначала сделаем функцию из кода проверки

// функция нам должна вернуть обработанное значение, то бишь int
public static int processSizeInput(String inputLine)
{
    int matrixSize; // звели переменную
    if (!int.TryParse(inputLine, out matrixSize))
    {
        Console.WriteLine("Вы ввели не число");
        continue; // <<< ЭТО БОЛЬШЕ НЕ РАБОТАЕТ
    }
    // теперь проверяем размерность
    if (!(matrixSize > 0 && matrixSize < 10))
    {
        Console.WriteLine("Число должно быть в переделах от 0 до 10");
        continue; // <<< ЭТО БОЛЬШЕ НЕ РАБОТАЕТ
    }
    return matrixSize;
}

вот эти два continue нам теперь портят всю картинку, такой код даже не скомпилируется.

Добавляем бросание исключений

Нам надо как-то сообщить из функцию что что-то пошло не так. Но как это сделать?

Для этого как нельзя лучше подойдет возможность бросать исключения.

Делается это очень просто, правим код

public static int processSizeInput(String inputLine)
{
    int matrixSize;
    if (!int.TryParse(inputLine, out matrixSize))
    {
        /* убираем это
        Console.WriteLine("Вы ввели не число");
        continue;

        и заменяем на:
        */ 
        throw new FormatException("Вы ввели не число");
    }
    // теперь проверяем размерность
    if (!(matrixSize > 0 && matrixSize < 10))
    {
        /* тут тоже убираем
        Console.WriteLine("Число должно быть в переделах от 0 до 10");
        continue;

        и заменяем:
        */
        throw new FormatException("Число должно быть в переделах от 0 до 10");
    }
    return matrixSize;
}

мы теперь убили двух зайцев, во первых у нас больше нет continue и код успешно скомпилируется, во вторых мы очистили код от вывода значения на Console, что является элементом взаимодействия с пользователем и не должно присутствовать в логике.

Теперь нам надо эту функцию подключить в main. Тут немного хитро.

Перехватываем исключения

Так как мы генерируем исключения надо их перехватывать. Для этого воспользуемся блоком try. Получится как-то так

static void Main(string[] args)
{
    int matrixSize;
    while (true) // << это не трогаем
    {
        var input = Console.ReadLine(); // << и это не трогаем
        
        /* А ТУТ ПОШЛА МАГИЯ ~~~ */
        try // это слово означает что внутри следующего блока мы будем пытаться поймать ошибку, типа ловушку поставили
        {
            // тут вызываем нашу функцию которая проверит ввод на корректность
            // и может выкинуть ошибку если отправленный input не корректен
            // зато если все ввели правильно то в matrixSize окажется размер матрицы
            matrixSize = processSizeInput(input);

            // это очень важная строчка, если мы дошли до сюда, 
            // значит ввод был корректен и можно выйти из бесконечного цикла 
            break; 
        }
        catch (FormatException ex) // ловим ошибку типа FormatException (см. выше мы там делали как раз new FormatException
        {   
            // если мы попали сюда, значит произошла ошибка FormatException
            // у ошибки ex, есть свойство сообщение
            // это как раз то что мы писали внутри processSizeInput
            // то есть если там было throw new FormatException("Вы ввели не число");
            // то ex.Message содержит "Вы ввели не число"
            // вот мы его и выводим
            Console.WriteLine(ex.Message);

             // следующую строчку в принцие можно не писать, 
            // но так нагляднее, типа явно отправляем пользователя на второй круг
            continue;
        }
    }

    Console.ReadKey();
}

Тестируем обработку ввода

теперь чтобы можно было протестировать этот код делаем класс публичным:

namespace ConsoleApp3
{
    public class Program // добавляем слово public
    {
        public static int processSizeInput(String inputLine)
        {
            /* ... */
        }
    }
    // ...
}

и тыкаем правой кнопкой как обычно

далее ок

ну и дальше пишем три теста (так как у нас три варианта исхода для функции)

namespace ConsoleApp3.Tests
{
    [TestClass()]
    public class ProgramTests
    {
        [TestMethod()]
        public void ProcessSizeInputNotANumberTest()
        {   
            // тут хитрый синтаксис, Assert.ThrowsException -- это метод для проверки что произойдет исключение
            // <FormatException> -- это дженерик, который будет проверять что выпадет исключение именно типа FormatException
            // () => { /* ... */ } -- это так называемое лямбда выражение, упрощённая форма записи для функции
            Assert.ThrowsException<FormatException>(() =>
            {   
                // и внутри этой лямбда-функции мы вызываем метод, который выкинет исключение 
                Program.processSizeInput("не число");
            });
        }
    }
}

если хочется еще проверить и сообщение, то делаем так

[TestMethod()]
public void ProcessSizeInputNotANumberTest()
{
    String message = Assert.ThrowsException<FormatException>(() =>
    {
        Program.processSizeInput("не число");
    }).Message;

    Assert.AreEqual("Вы ввели не число", message);
}

второй тест пишем по аналогии, то есть когда число выходит за рамки:

[TestClass()]
public class ProgramTests
{
    [TestMethod()]
    public void ProcessSizeInputNotANumberTest() { /* ... */ }

    [TestMethod()]
    public void ProcessSizeInputOutOfLimitsTest()
    {
        String message = Assert.ThrowsException<FormatException>(() =>
        {
            Program.processSizeInput("12");
        }).Message;

        Assert.AreEqual("Число должно быть в переделах от 0 до 10", message);
    }
}

ну и третий тест на корректный ввод

[TestMethod()]
public void ProcessSizeInputTest()
{
    Assert.AreEqual(5, Program.processSizeInput("5"));
}

Как быть с матрицей

если вы дополнительно запрашиваете элементы матрицы то просто делаете дополнительную функцию а цикл while пихаете внутрь двойного цикла как-то так:

public static int processSizeInput(String inputLine) { /* ... */ }

// добавил новую функцию
public static int processCellInput(String inputLine)
{
    if (!int.TryParse(inputLine, out int a))
    {
        throw new FormatException("Вы ввели не число");
    }
    return a;
}

static void Main(string[] args)
{
    /* ЭТО НЕ ТРОГАЕМ */
    int matrixSize;
    while (true)
    {
        var input = Console.ReadLine();
        try
        {
            matrixSize = processSizeInput(input);
            break;
        }
        catch (FormatException ex)
        {
            Console.WriteLine(ex.Message);
            continue;
        }
    }
    /* КОНЕЦ ЭТО НЕ ТРОГАЕМ */
    
    // создаем двумерный массив
    var data = new int[matrixSize, matrixSize];
    // затем двумерный цикл
    for (int i = 0; i < matrixSize; ++i)
    {
        for (int j = 0; j < matrixSize; ++j)
        {   
            // сообщенице
            Console.WriteLine($"Введите [{i}, {j}] элемент");
            // а тут все тоже самое что и с запросом размерности, только фиксируем результат в data[i, j]
            while(true)
            {
                var input = Console.ReadLine();
                try
                {
                    data[i, j] = processCellInput(input);
                    break;
                }
                catch (FormatException ex)
                {
                    Console.WriteLine(ex.Message);
                    continue;
                }
            }
        }
    }

    Console.ReadKey();
}

вот такие дела =)