【问题标题】:In MSTest, How can I verify exact error message using [ExpectedException(typeof(ApplicationException))]在 MSTest 中,如何使用 [ExpectedException(typeof(ApplicationException))] 验证确切的错误消息
【发布时间】:2010-12-29 00:32:35
【问题描述】:

使用 MSTest 如何验证来自测试方法的确切错误消息?我知道[ExpectedException(typeof(ApplicationException), error msg)] 不会比较来自我的测试方法的错误消息,尽管在其他单元测试框架中它正在这样做。

解决这个问题的一种方法是使用一些 try catch 块来编写我的单元测试,但是我需要再写 4 行。

有没有最聪明的方法来检查错误信息。

干杯, 普里坦

【问题讨论】:

  • 固定标题。不知道 MSTest 没有这个...应该很容易支持

标签: unit-testing automation mstest


【解决方案1】:

此代码在 async/await 场景中执行此操作:

Code Reference

public async static Task AssertThrowsAsync<T>(Task task, string expectedMessage) where T : Exception
{
    try
    {
        await task;
    }
    catch (Exception ex)
    {
        if (ex is T)
        {
            Assert.AreEqual(expectedMessage, ex.Message);
            return;
        }

        Assert.Fail($"Expection exception type: {typeof(T)} Actual type: {ex.GetType()}");
    }

    Assert.Fail($"No exception thrown");
}

示例用法: Code Reference

    [TestMethod]
    public async Task TestBadRequestThrowsHttpStatusCodeException()
    {
        var mockHttp = new MockHttpMessageHandler();

        const HttpStatusCode statusCode = HttpStatusCode.BadRequest;

        mockHttp.When("https://restcountries.eu/rest/v2/")
                .Respond(statusCode, "application/json", JsonConvert.SerializeObject(new { Message = "Test", ErrorCode = 100 }));

        var httpClient = mockHttp.ToHttpClient();

        var factory = new SingletonHttpClientFactory(httpClient);

        var baseUri = new Uri("https://restcountries.eu/rest/v2/");
        var client = new Client(new NewtonsoftSerializationAdapter(), httpClientFactory: factory, baseUri: baseUri, logger: _logger.Object);

        await AssertThrowsAsync<HttpStatusException>(client.GetAsync<List<RestCountry>>(), Messages.GetErrorMessageNonSuccess((int)statusCode, baseUri));
    }

这相当于同步情况

public static void AssertThrows<T>(Action action, string expectedMessage) where T : Exception
{
    try
    {
        action.Invoke();
    }
    catch (Exception ex)
    {
        if (ex is T)
        {
            Assert.AreEqual(expectedMessage, ex.Message);
            return;
        }

        Assert.Fail($"Expection exception type: {typeof(T)} Actual type: {ex.GetType()}");
    }

    Assert.Fail($"No exception thrown");
}

注意:这断言异常继承自给定类型。如果要检查特定类型,则应检查类型相等性,而不是使用 is 运算符。

【讨论】:

    【解决方案2】:

    Fluent Assertions (NuGet) 有一种非常“自然语言”的语法来定义单元测试中的期望:

    objectundertest.Invoking(o => o.MethodUnderTest()).Should().Throw<ExpectedException>()
        .WithMessage("the expected error message");
    

    使用任何算法 (Where(e =&gt; ...) 检查错误消息以及检查内部异常及其消息有多种变体。

    【讨论】:

    • 轻微语法错误 ::: "ShouldThrow" 应该是 "Should().Throw"(至少对于 "FluentAssertions" Version="5.10.0")谢谢 MarkL!
    • 未来读者,一些文档:fluentassertions.com/exceptions
    【解决方案3】:

    MSTest v2 支持返回捕获的异常的 Assert.Throws 和 Assert.ThrowsAsync。

    这是一篇关于如何升级到 MSTest v2 的文章:https://blogs.msdn.microsoft.com/devops/2017/09/01/upgrade-to-mstest-v2/

    这里是示例用法:

    var myObject = new MyObject();
    var ex = Assert.Throws<ArgumentNullException>(() => myObject.Do(null));
    StringAssert.Contains(ex.Message, "Parameter name: myArg");
    

    【讨论】:

      【解决方案4】:

      注释和 try/catch 块的烦恼在于,您没有明确区分测试的 ACT 和 ASSERT 阶段。一种更简单的方法是使用以下实用程序“捕获”异常作为 ACT 阶段的一部分:

      public static class Catch
      {
          public static Exception Exception(Action action)
          {
              Exception exception = null;
      
              try
              {
                  action();
              }
              catch (Exception ex)
              {
                  exception = ex;
              }
      
              return exception;
          }
      }
      

      这允许你做:

      // ACT
      var actualException = Catch.Exception(() => DoSomething())
      
      // ASSERT
      Assert.IsNotNull(actualException, "No exception thrown");
      Assert.IsInstanceOfType(actualException, expectedType);
      Assert.AreEqual(expectedExceptionMessage, actualException.Message);
      

      【讨论】:

        【解决方案5】:

        你可以create your own ExpectedException attribute 在那里你可以Assert 被抛出的Exception 的消息。

        代码

        namespace TestProject
        {
            public sealed class MyExpectedException : ExpectedExceptionBaseAttribute
            {
                private Type _expectedExceptionType;
                private string _expectedExceptionMessage;
        
                public MyExpectedException(Type expectedExceptionType)
                {
                    _expectedExceptionType = expectedExceptionType;
                    _expectedExceptionMessage = string.Empty;
                }
        
                public MyExpectedException(Type expectedExceptionType, string expectedExceptionMessage)
                {
                    _expectedExceptionType = expectedExceptionType;
                    _expectedExceptionMessage = expectedExceptionMessage;
                }
        
                protected override void Verify(Exception exception)
                {
                    Assert.IsNotNull(exception);
        
                    Assert.IsInstanceOfType(exception, _expectedExceptionType, "Wrong type of exception was thrown.");
        
                    if(!_expectedExceptionMessage.Length.Equals(0))
                    {
                        Assert.AreEqual(_expectedExceptionMessage, exception.Message, "Wrong exception message was returned.");
                    }
                }
            }
        }
        

        用法

        [TestMethod]
        [MyExpectedException(typeof(Exception), "Error")]
        public void TestMethod()
        {
            throw new Exception("Error");
        }
        

        【讨论】:

        • 我喜欢...检查自定义异常消息的好方法。 :)
        【解决方案6】:

        我正在寻找一种方法来使用 mstest 检查内部异常的存在和类型,我发现了这个问题。我知道这是一个 2 年前的话题,但由于我的解决方案不在这里,让我分享一下。

        对我来说,解决问题最优雅的方法是创建派生属性,这是我的(抱歉,cmets 和字符串是法语,我的自然语言,但应该很明显):

        #region Références
        using System;
        using Microsoft.VisualStudio.TestTools.UnitTesting;
        using System.Text.RegularExpressions;
        #endregion
        
        namespace MsTestEx
        {
            /// <summary>
            /// Extention de l'attribut ExpectedException permettant de vérifier plus d'éléments (Message, InnerException, ...)
            /// </summary>
            public class ExpectedExceptionEx
                : ExpectedExceptionBaseAttribute
            {
                #region Variables locales
                private Type    _ExpectedException              = null;
                private string  _ExpectedMessage                = null;
                private Type    _ExpectedInnerException         = null;
                private string  _ExpectedInnerExceptionMessage  = null;
                private bool    _IsExpectedMessageRegex         = false;
                private bool    _IsExpectedInnerMessageRegex    = false;
                private bool    _AllowDerivedType               = false;
                private bool    _AllowInnerExceptionDerivedType = false;
        
                private bool    _CheckExpectedMessage           = false;
                private bool    _CheckInnerExceptionType        = false;
                private bool    _CheckInnerExceptionMessage     = false;
                #endregion
        
                #region Propriétés
                /// <summary>
                /// Vérifie que le message de l'exception correspond à celui-ci.
                /// </summary>
                public string ExpectedMessage
                {
                    get { return _ExpectedMessage; }
                    set { _ExpectedMessage = value; _CheckExpectedMessage = true; }
                }
        
                /// <summary>
                /// Vérifie que le message de l'inner-exception correspond à celui-ci.
                /// </summary>
                public string ExpectedInnerExceptionMessage
                {
                    get { return _ExpectedInnerExceptionMessage; }
                    set { _ExpectedInnerExceptionMessage = value; _CheckInnerExceptionMessage = true; }
                }
        
                /// <summary>
                /// Vérifie que l'exception possède bien une inner-exception du type spécifié.
                /// Spécifier "null" pour vérifier l'absence d'inner-exception.
                /// </summary>
                public Type ExpectedInnerException
                {
                    get { return _ExpectedInnerException; }
                    set { _ExpectedInnerException = value; _CheckInnerExceptionType = true; }
                }
        
                /// <summary>
                /// Indique si le message attendu est exprimé via une expression rationnelle.
                /// </summary>
                public bool IsExpectedMessageRegex
                {
                    get { return _IsExpectedMessageRegex; }
                    set { _IsExpectedMessageRegex = value; }
                }
        
                /// <summary>
                /// Indique si le message attendu de l'inner-exception est exprimé via une expression rationnelle.
                /// </summary>
                public bool IsExpectedInnerMessageRegex
                {
                    get { return _IsExpectedInnerMessageRegex; }
                    set { _IsExpectedInnerMessageRegex = value; }
                }
        
                /// <summary>
                /// Indique si les exceptions dérivées sont acceptées.
                /// </summary>
                public bool AllowDerivedType
                {
                    get { return _AllowDerivedType; }
                    set { _AllowDerivedType = value; }
                }
        
                /// <summary>
                /// Indique si les inner-exceptions dérivées sont acceptées.
                /// </summary>
                public bool AllowInnerExceptionDerivedType
                {
                    get { return _AllowInnerExceptionDerivedType; }
                    set { _AllowInnerExceptionDerivedType = value; }
                }
                #endregion
        
                #region Constructeurs
                /// <summary>
                /// Indique le type d'exception attendu par le test.
                /// </summary>
                /// <param name="expectedException">Type de l'exception attendu.</param>
                public ExpectedExceptionEx(Type expectedException)
                {
                    _ExpectedException = expectedException;
                }
                #endregion
        
                #region Méthodes
                /// <summary>
                /// Effectue la vérification.
                /// </summary>
                /// <param name="exception">Exception levée.</param>
                protected override void Verify(Exception exception)
                {
                    Assert.IsNotNull(exception); // Pas eu d'exception, ce n'est pas normal
        
                    // Vérification du type de l'exception
                    Type actualType = exception.GetType();
                    if (_AllowDerivedType) Assert.IsTrue(_ExpectedException.IsAssignableFrom(actualType), "L'exception reçue n'est pas du type spécifié ni d'un type dérivé.");
                    else Assert.AreEqual(_ExpectedException, actualType, "L'exception reçue n'est pas du type spécifié.");
        
                    // Vérification du message de l'exception
                    if (_CheckExpectedMessage)
                    {
                        if (_IsExpectedMessageRegex)
                            Assert.IsTrue(Regex.IsMatch(exception.Message, _ExpectedMessage), "Le message de l'exception ne correspond pas à l'expression rationnelle");
                        else
                        {
                            string s1, s2;
                            if (exception.Message.Length > _ExpectedMessage.Length)
                            {
                                s1 = exception.Message;
                                s2 = _ExpectedMessage;
                            }
                            else
                            {
                                s1 = _ExpectedMessage;
                                s2 = exception.Message;
                            }
                            Assert.IsTrue(s1.Contains(s2), "Le message de l'exception ne contient pas et n'est pas contenu par le message attendu.");
                        }
                    }
        
                    if (_CheckInnerExceptionType)
                    {
                        if (_ExpectedInnerException == null) Assert.IsNotNull(exception.InnerException);
                        else
                        {
                            // Vérification du type de l'exception
                            actualType = exception.InnerException.GetType();
                            if (_AllowInnerExceptionDerivedType) Assert.IsTrue(_ExpectedInnerException.IsAssignableFrom(actualType), "L'inner-exception reçue n'est pas du type spécifié ni d'un type dérivé.");
                            else Assert.AreEqual(_ExpectedInnerException, actualType, "L'inner-exception reçue n'est pas du type spécifié.");
                        }
                    }
        
                    if (_CheckInnerExceptionMessage)
                    {
                        Assert.IsNotNull(exception.InnerException);
                        if (_IsExpectedInnerMessageRegex)
                            Assert.IsTrue(Regex.IsMatch(exception.InnerException.Message, _ExpectedInnerExceptionMessage), "Le message de l'exception ne correspond pas à l'expression rationnelle");
                        else
                        {
                            string s1, s2;
                            if (exception.InnerException.Message.Length > _ExpectedInnerExceptionMessage.Length)
                            {
                                s1 = exception.InnerException.Message;
                                s2 = _ExpectedInnerExceptionMessage;
                            }
                            else
                            {
                                s1 = _ExpectedInnerExceptionMessage;
                                s2 = exception.InnerException.Message;
                            }
                            Assert.IsTrue(s1.Contains(s2), "Le message de l'inner-exception ne contient pas et n'est pas contenu par le message attendu.");
                        }
                    }
                }
                #endregion
            }
        }
        

        现在,将此属性与命名参数一起使用,而不是“ExpectedException”。使用我的属性,您可以检查是否存在内部异常、异常消息和内部异常、使用正则表达式匹配消息等... 您可以根据需要进行调整。

        【讨论】:

          【解决方案7】:

          使用这个小助手类:

          public static class ExceptionAssert
          {
              public static void Throws<TException>(Action action, string message)
                  where TException : Exception
              {
                  try
                  {
                      action();
          
                      Assert.Fail("Exception of type {0} expected; got none exception", typeof(TException).Name);
                  }
                  catch (TException ex)
                  {
                      Assert.AreEqual(message, ex.Message);
                  }
                  catch (Exception ex)
                  {
                      Assert.Fail("Exception of type {0} expected; got exception of type {1}", typeof(TException).Name, ex.GetType().Name);               
                  }
              }
          }
          

          用法:

          Foo foo = new Foo();
          foo.Property = 42;
          
          ExceptionAssert.Throws<InvalidOperationException>(() => foo.DoSomethingCritical(), "You cannot do anything when Property is 42.");
          

          显式捕获异常的优点是当另一个成员(例如在初始化期间)抛出异常时测试不会成功。

          【讨论】:

          • 不错!我将使用您的方法并将其扩展为断言 InnerException :D
          • 第一个 Assert.Fail 不应该在最后,在 try/catch 块之外吗?如果你把它放在 try 块内,那么它将被第二个 catch 块捕获。这会导致两个异常。
          • @Rhyous Assert.Fail 的解决方案是在第一个 Catch 块中添加以下内容 catch(AssertFailedException){ throw; }
          • Hmm...这与解决方案是将 try 块中的 Assert 移动到 try 块之外的末尾一样有效。不过,我觉得不对。代码气味?测试代码令人困惑和不清楚,可能会让读者相信 action() 可能会抛出 AssertFailedException 错误。 try 应该只尝试 action() 代码。捕获的异常应该只是 action() 可以实际抛出的异常。 try 块不应该只是“尝试” Assert.Fail 代码。
          【解决方案8】:

          在 MSTest 中没有内置的方法。这几乎是“优雅”的:

          [TestMethod]
          public void Test8()
          {
              var t = new Thrower();
              try
              {
                  t.DoStuffThatThrows();
                  Assert.Fail("Exception expected.");
              }
              catch (InvalidOperationException e)
              {
                  Assert.AreEqual("Boo hiss!", e.Message);
              }
          }
          

          但是,您可以考虑将 xUnit.NET 的 Assert.Throws API 移植到自定义库 - 我们就是这样做的。

          您也可以通过vote on this suggestion 访问 Microsoft Connect。

          【讨论】:

            【解决方案9】:

            MbUnit 也可以这样做:

            [Test]
            [Row(ExpectedExceptionMessage="my message")]
            void TestBlah(...
            

            【讨论】:

              【解决方案10】:

              更新:哎呀.. 看到你想要这个在 MSTest。对不起。快速阅读并被您的标题误导。

              试试这个来自Callum Hibbert扩展项目,看看它是否有效。

              旧回复:

              您可以使用 NUnit 2.4 及更高版本执行此操作。 请参阅此处的 ExpectedException 的 documentation

              [ExpectedException( typeof( ArgumentException), ExpectedMessage="unspecified", MatchType=MessageMatch.Contains )]
              public void TestMethod()
              {
              ...
              

              MatchType 可以是 Exact(默认)、Contains 或 Regex.. 几乎可以处理 80% 的用例。如果验证变得太复杂,还有一种高级异常处理方法方法。个人从未使用过它。暂时不需要它。

              【讨论】:

                【解决方案11】:

                使用 MSTest,您无法做到这一点。

                你已经知道这个问题的解决方案:在 catch 块中断言异常消息。

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2017-08-19
                  • 1970-01-01
                  • 2020-07-18
                  • 1970-01-01
                  • 1970-01-01
                  • 2013-07-16
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多