通过事件 C# 处理异常的单元测试方法

Unit Testing Methods with Handled Exceptions Via Events C#

我决定为我拥有的 WinForm 应用程序创建单元测试。我使用了自定义分层 MVC 方法(视图、控制器、模型),它通过创建模拟视图和测试控制器方法使单元测试更加容易。我遇到了一个有趣的异常问题。在我的应用程序中,异常是通过事件传播的。我的控制器订阅了包含在具有异常信息的模型中的 "Exception Event"。在事件处理程序中,控制器获取该信息并调用视图的 "Display Error" 方法。这里有一些部分代码来描述我在做什么:

interface IView
{
    public void DisplayError(string message);
}

public class Controller
{
    IView _view;
    Model _model;
    public Controller(IView view, Model model)
    {
        _view = view;
        _model = model;
        model.ErrorRaised += ErrorRaisedEventHandler(handle_error);
    }

    private void handle_error(object sender, ErrorEventArgs e)
    {
        _view.DisplayError(e.Message);
    }
}

public Model
{
    event ErrorRaisedEventHandler ErrorRaised;

    public void DoSomething()
    {
        try
        {
            //Do something bad
        }
        catch (Exception e)
        {
            ErrorRaised(this, new ErrorEventArgs(e.Message))
        }
    }
}

是否有对此进行单元测试的最佳实践?我一直在寻找关于从异常中断言输出消息的内容,但并没有走得太远。谢谢!

您的代码甚至无法编译,public Model 缺失 class 并且 model.ErrorRaised += ErrorRaisedEventHandler 缺失新的。下次请做必要的,因为它会阻止人们...

工作代码

public interface IView
{
    void DisplayError(string message);
}

public class MyView : IView
{
    public string ErrorMessage;
    public void DisplayError(string message)
    {
        ErrorMessage = message;
        System.Diagnostics.Debug.Write(message);
    }
}

public delegate void ErrorRaisedEventHandler(object sender, ErrorEventArgs e);

public class Controller
{
    IView _view;
    Model _model;
    public Controller(IView view, Model model)
    {
        _view = view;
        _model = model;
        _model.ErrorRaised += new ErrorRaisedEventHandler((s,e) => _view.DisplayError(e.GetException().Message));
    }
}

public class Model
{
    public event ErrorRaisedEventHandler ErrorRaised;

    int monthsAlive = 0;
    public int MonthsAliveInPlanet(int yearBorn, int yearInTime, int monthsInPlanetsYear)
    {
        try
        {
            //Do something bad - divisioin by zero
            monthsAlive = (yearInTime - yearBorn) / monthsInPlanetsYear;               
        }
        catch (Exception e)
        {
            ErrorRaised(this, new ErrorEventArgs(e));
        }
        return monthsAlive;
    }
}

最佳实践

一般来说,你应该把所有的逻辑都放在控制器中而不是模型中。但是,为了娱乐您通过控制器从模型向视图引发异常事件的示例,我编写了一个方法:MonthsAliveInPlanet。

有时模型中会出现一些逻辑,比如年龄计算 DateTime.Now.Year - yearBorn 以及模拟被零除异常的简单内容我使用了 "monthsInPlanetsYear".

是否有对此进行单元测试的最佳实践?

可能不会,您最好将逻辑移至控制器并对其进行测试。不过,如果您确实想 勾选 "best practice" 框 并为其编写单元测试,我建议您使用 VS2015 中包含的 "Create IntelliTests" 功能:

这将生成以下通用单元测试代码(如上所示,有 2 个失败测试和 1 个通过测试),您可以在此基础上测试所有边缘情况 .

[PexClass(typeof(Model))]
[PexAllowedExceptionFromTypeUnderTest(typeof(InvalidOperationException))]
[PexAllowedExceptionFromTypeUnderTest(typeof(ArgumentException), AcceptExceptionSubtypes = true)]
[TestClass]
public partial class ModelTest
{
    /// <summary>Test stub for MonthsAliveInPlanet(Int32, Int32, Int32)</summary>
    [PexMethod]
    public int MonthsAliveInPlanetTest([PexAssumeUnderTest]Model target, int yearBorn, int yearInTime, int monthsInPlanetsYear)
    {
        int result = target.MonthsAliveInPlanet(yearBorn, yearInTime, monthsInPlanetsYear);
        return result;
        // TODO: add assertions to method ModelTest.MonthsAliveInPlanetTest(Model, Int32, Int32, Int32)
    }
}

边缘案例

所以我添加的一个边缘案例来测试被零除错误消息冒泡到视图:

[TestMethod]
/// <summary>Test stub for MonthsAliveInPlanet(Int32, Int32, Int32)</summary>
public void MonthsAliveInPlanetTestShouldFailWithZeroMonthsInYear()
{
    //Arrange
    IView view = new MyView();
    Model target = new Model();
    Controller ctrl = new Controller(view, target);                        
    int yearBorn = 0;
    int yearInTime = 0;
    int monthsInPlanetsYear = 0;

    //Act
    int result = target.MonthsAliveInPlanet(yearBorn, yearInTime, monthsInPlanetsYear);

    //Assert
    Assert.AreEqual("Attempted to divide by zero.",((MyView)view).ErrorMessage);
}

尽管我接受了在模型中使用微不足道的逻辑并在发生异常时冒出一条错误消息的想法,但这不是好的做法

使用 DataAnnotations 属性验证模型数据

异常是异常的,模型应该是愚蠢的,这样任何视图都可以绑定到它,而控制器则负责工作。最好的做法是将所有业务逻辑放在控制器中并使用 DataAnnotations Attributes 来验证模型,您不希望模型中出现异常,例如:

public class Demo {
    [StringLength(50),Required]
    public object Name { get; set; }
    [StringLength(15)]
    public object Color { get; set; }
    [Range(0, 9999)]
    public object Weight { get; set; }
}