You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何未抛出异常?(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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 19:55:19