为何未抛出异常?(C#/NUnit/.NET 6环境)
我是C#新手,正在测试控制器但测试无法正常运行。本次测试我期望触发自定义异常InvalidQueryException,但运行后得到提示:
"Expected: <RedAeris.Exceptions.InvalidQueryException>
But was: null"
我怀疑代码并未实际执行,因为断点和打印语句都不生效。以下是控制器及测试代码:
控制器代码
using Microsoft.AspNetCore.Mvc; using RedAeris.Constants; using RedAeris.Exceptions; using RedAeris.Models; using RedAeris.Utilities; using System.Text.Json; namespace RedAeris.Controllers { /// <summary> /// This class is responsible for gathering data for symbols. /// </summary> [Route("api/[controller]")] [ApiController] public class TickerHistoryController : ControllerBase { /// <summary> /// This function is used to get data for a symbol based on a ticker query and return the returns for an investment. /// </summary> /// <param name="tickerQuery"></param> /// <returns>Return the returns for a given investment.</returns> /// <exception cref="Exceptions.InvalidQueryException"></exception> // GET api/<TickerHistory>/tickerQuery?fromDate={yyyy-MM-dd}&toDate={yyyy-MM-dd}&tickerSymbol={SYMBOL}' [HttpGet("tickerQuery")] public async Task<List<double>> TickerData([FromQuery] TickerQuery tickerQuery) { System.Diagnostics.Debug.WriteLine("HIT"); if (string.IsNullOrEmpty(tickerQuery.tickerSymbol)) { throw new InvalidQueryException(InvalidQueryException.MISSING_TICKER_SYMBOL_MESSAGE); // Can't perform search without a symbol } if (string.IsNullOrEmpty(tickerQuery.fromDate.ToString())) { tickerQuery.fromDate = new DateTime(DateTime.Now.Year, 1, 1); } // toDate is missing if (string.IsNullOrEmpty(tickerQuery.toDate.ToString())) { tickerQuery.toDate = DateTime.Today; } // Too long of query if ((tickerQuery.toDate - tickerQuery.fromDate).TotalDays > IEX.MAX_QUERY_TIMERANGE * IEX.DAYS_IN_YEAR) { throw new InvalidQueryException(InvalidQueryException.MAX_TIMERANGE_EXCEEDED_MESSAGE); } // Start time comes after end if ((tickerQuery.toDate - tickerQuery.fromDate).TotalDays < 0) { throw new InvalidQueryException(InvalidQueryException.INVALID_START_AND_END_DATE_COMBINATION_MESSAGE); } using (var client = new HttpClient()) { var url = IEX.urlBuilder(tickerQuery); // Creates URL for API call client.BaseAddress = new Uri(url); HttpResponseMessage response = await client.GetAsync(url); if (response.IsSuccessStatusCode) { string jsonData = await response.Content.ReadAsStringAsync(); System.Diagnostics.Debug.WriteLine(jsonData); List<TickerData> data = JsonSerializer.Deserialize<List<TickerData>>(jsonData); // Map JSON to TickerData list return ReturnCalculator.calculateReturn(data); } else { return null; } } } } }
测试代码
using Microsoft.AspNetCore.Mvc.Testing; using NUnit.Framework; using RedAeris.Exceptions; namespace RedAeris.Tests.Controllers { [TestFixture] public class TickerHistoryControllerTests { private WebApplicationFactory<Program> factory; [SetUp] public void Setup() { factory = new WebApplicationFactory<Program>(); } [TearDown] public void Teardown() { factory.Dispose(); } [Test] public async Task ShouldThrowInvalidQueryExceptionWithNoTicker() { var client = factory.CreateClient(); var url = "https://localhost:7267/api/TickerHistory/tickerQuery?fromDate=2023%2F04%2F01&toDate=2023%2F04%2F20"; // Act & Assert Assert.ThrowsAsync<InvalidQueryException>(async () => await client.GetAsync(url)); // Assert.AreEqual("Response status code does not indicate success: 404 (Not Found).", exception.Message); } } }
1. 测试请求未命中内存测试服务器
你在测试中硬编码了https://localhost:7267,但WebApplicationFactory.CreateClient()生成的客户端默认指向内存中的测试服务器,不需要指定完整域名。硬编码URL会让请求发到真实本地服务器(甚至可能没启动),导致控制器代码完全没执行,断点和打印语句自然无效。
修复:改用相对路径请求,同时修正日期格式为yyyy-MM-dd(避免模型绑定失败):
var url = "/api/TickerHistory/tickerQuery?fromDate=2023-04-01&toDate=2023-04-20";
2. ASP.NET Core 会自动捕获异常并返回HTTP响应
控制器中抛出的InvalidQueryException不会直接传播到测试客户端——ASP.NET Core的默认异常处理机制会把异常转换成HTTP错误响应(默认500状态码)。因此Assert.ThrowsAsync根本捕获不到预期的异常,只能得到null。
正确做法:验证响应的状态码和内容:
[Test] public async Task ShouldReturnBadRequestWhenNoTickerProvided() { var client = factory.CreateClient(); var url = "/api/TickerHistory/tickerQuery?fromDate=2023-04-01&toDate=2023-04-20"; // Act var response = await client.GetAsync(url); // Assert Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); var content = await response.Content.ReadAsStringAsync(); Assert.Contains(InvalidQueryException.MISSING_TICKER_SYMBOL_MESSAGE, content); }
如果希望自定义异常对应的HTTP状态码,可以添加异常过滤器,或者在抛出异常时使用Problem()方法返回指定状态:
if (string.IsNullOrEmpty(tickerQuery.tickerSymbol)) { return Problem( detail: InvalidQueryException.MISSING_TICKER_SYMBOL_MESSAGE, statusCode: StatusCodes.Status400BadRequest, title: "Invalid Query"); }
3. 模型绑定存在潜在问题
你的TickerQuery模型中,fromDate和toDate如果是DateTime(非可空),当URL未传值时,模型绑定会直接失败,控制器方法不会执行。建议改为可空类型:
public class TickerQuery { public string tickerSymbol { get; set; } public DateTime? fromDate { get; set; } public DateTime? toDate { get; set; } }
同时修正控制器中的空值判断逻辑,避免调用null.ToString()引发空引用异常:
if (!tickerQuery.fromDate.HasValue) { tickerQuery.fromDate = new DateTime(DateTime.Now.Year, 1, 1); }
4. 断点不生效的解决
修正测试URL为相对路径后,测试会使用内存中的测试服务器运行,此时断点就能正常触发,打印语句也会输出到测试控制台。
内容的提问来源于stack exchange,提问作者Michael Rutkowski

