Как поймать исключение в MSTest и не сойти с ума?
Вы пишете юнит-тесты на C# и вдруг понимаете: ваш метод должен выбросить исключение в определённой ситуации. Как это проверить? Если вы гуглили «How do I use Assert to verify that an exception has been thrown with MSTest», то вы не одиноки — это один из самых популярных вопросов на Stack Overflow. И сегодня мы разберём его раз и навсегда.
В этой статье я покажу вам все способы проверки исключений в MSTest: от классического атрибута до современного Assert.ThrowsException. Вы узнаете, как писать чистые, надёжные тесты, которые не упадут в самый неподходящий момент. Поехали!
Почему проверка исключений — это важно?
Представьте: у вас есть метод Divide(int a, int b). Если b = 0, он должен выбросить DivideByZeroException. Без теста вы узнаете об этом только когда приложение упадёт в production. Unit-тест с проверкой исключения — это ваш страховочный трос.
В MSTest (фреймворк для модульного тестирования от Microsoft) есть несколько подходов. Мы рассмотрим каждый, но начнём с самого современного и рекомендуемого.
Метод 1: Assert.ThrowsException (Самый правильный)
Начиная с MSTest V2 (версия 2.x), у нас есть встроенный метод Assert.ThrowsException<T>. Это элегантно, читаемо и гибко. Синтаксис простой:
[TestMethod]
public void Divide_ByZero_ThrowsDivideByZeroException()
{
// Arrange
var calculator = new Calculator();
// Act & Assert
Assert.ThrowsException<DivideByZeroException>(() => calculator.Divide(10, 0));
}
Что здесь происходит? Мы передаём лямбда-выражение с вызовом метода. MSTest выполняет его внутри блока try-catch. Если исключение НЕ выбрасывается — тест падает. Если выбрасывается исключение другого типа — тоже падает. Только нужное исключение приводит к успеху.
Продвинутый пример: проверка сообщения исключения
Иногда нужно убедиться не только в типе исключения, но и в его сообщении. Для этого используйте перегрузку с параметром message или проверяйте свойства объекта исключения:
[TestMethod]
public void GetUserById_InvalidId_ThrowsWithCorrectMessage()
{
// Arrange
var service = new UserService();
// Act
var exception = Assert.ThrowsException<ArgumentException>(() => service.GetUserById(-1));
// Assert дополнительно
Assert.AreEqual("Id must be positive", exception.Message);
}
Обратите внимание: Assert.ThrowsException возвращает объект исключения! Это позволяет вам проверить любые его свойства: InnerException, Data, StackTrace (хотя последний обычно не проверяют).
Метод 2: Атрибут [ExpectedException] (Устаревший, но всё ещё в ходу)
Раньше (в MSTest V1) использовался атрибут над самим методом теста. Выглядело это так:
[TestMethod]
[ExpectedException(typeof(DivideByZeroException))]
public void Divide_ByZero_ThrowsDivideByZeroException_OldWay()
{
var calculator = new Calculator();
calculator.Divide(10, 0);
}
Кажется удобно? Но есть три проблемы:
- Нельзя проверить сообщение исключения. Вы не знаете, то ли исключение выброшено.
- Любое исключение этого типа — success. Даже если оно вылетело из другой строки кода (например, в Arrange).
- Нельзя выполнить код после исключения. Метод теста прерывается сразу.
Поэтому в новых проектах используйте Assert.ThrowsException. Атрибут [ExpectedException] помечен как deprecated (устаревший) и может быть удалён в будущих версиях.
Метод 3: Ручной try-catch (Для экстремалов)
Иногда (очень редко) вам нужен полный контроль. Например, если вы тестируете асинхронный код или хотите проверить, что исключение выбрасывается в середине сложного метода. Тогда можно сделать так:
[TestMethod]
public void ProcessData_InvalidInput_ThrowsException_Manual()
{
// Arrange
var processor = new DataProcessor();
// Act
bool exceptionThrown = false;
try
{
processor.ProcessData(null);
}
catch (InvalidOperationException)
{
exceptionThrown = true;
}
// Assert
Assert.IsTrue(exceptionThrown, "Expected InvalidOperationException was not thrown.");
}
Этот метод даёт 100% контроль, но требует больше кода. Используйте его только когда Assert.ThrowsException не подходит (например, при тестировании многопоточности).
Асинхронные исключения: Assert.ThrowsExceptionAsync
Современный код полон асинхронных методов. Для них есть специальная версия:
[TestMethod]
public async Task FetchDataAsync_NetworkError_ThrowsHttpRequestException()
{
// Arrange
var client = new HttpClientWrapper();
// Act & Assert
await Assert.ThrowsExceptionAsync<HttpRequestException>(async () =>
await client.FetchDataAsync("http://invalid.url"));
}
Важно: метод теста должен быть async Task, а не async void. И не забудьте про await перед Assert.ThrowsExceptionAsync — иначе тест завершится до того, как исключение будет поймано.
Частые ошибки и как их избежать
Даже опытные разработчики иногда спотыкаются. Вот топ-3 ошибки при проверке исключений в MSTest:
Ошибка 1: Забыли про лямбду
Не делайте так:
// ❌ Неправильно!
Assert.ThrowsException<Exception>(calculator.Divide(10, 0));
Вы передаёте результат вызова метода, а не делегат. Исключение вылетит до того, как Assert начнёт работать. Правильно:
// ✅ Правильно!
Assert.ThrowsException<Exception>(() => calculator.Divide(10, 0));
Ошибка 2: Слишком общее исключение
Не ловите Exception без необходимости. Тест пройдёт, если метод выбросит NullReferenceException вместо ожидаемого ArgumentException. Это скроет баг. Всегда указывайте максимально конкретный тип.
Ошибка 3: Не проверяете состояние после исключения
Иногда нужно убедиться, что после исключения объект остался в консистентном состоянии. Assert.ThrowsException позволяет это сделать:
[TestMethod]
public void Withdraw_InsufficientFunds_ThrowsAndBalanceUnchanged()
{
// Arrange
var account = new BankAccount(100);
// Act
Assert.ThrowsException<InsufficientFundsException>(() => account.Withdraw(200));
// Assert