我正在使用三层架构中的Web表单和MVP模式创建一个购物网站。我还决定在presenter类中进行验证和输入。对于测试框架,我使用NUnit,对于我的模拟,我使用NSubstitude。这是我的类别模型类:
//we're doing Dependency injection here.
public abstract class BaseRepository
{
EntityContext context;
public BaseRepository()
{
context = new EntityContext();
}
public EntityContext Context
{
get { return context; }
}
}
public class CategoryRepository : BaseRepository
{
public int Add(long id, string name)
{
Category cat = new Category();
cat.Id = id;
cat.Name = name;
Context.Category.Add(cat);
Context.SaveChanges();
}
}
这是类别演示者:
public class CategoryPresenter : BasePresenter //has nothing but a dependency property to Logger
{
BaseRepository _model;
IView _view;
public CategoryPresenter(IView view)
{
_model = new CategoryRepository();
_view = view;
}
public void Add()
{
//havn't passed the tests yet since i'm not sure if i'm on the correct path.
//whatever validation, loggin and type casting will go here.
_model.Add(_view.CategoryId, _view.CategoryName);
}
}
以下是演示者的测试类:
[Test]
public void Add_NullId_ThrowException()
{
_view.CategoryId.Returns(p => null);
_view.CategoryName.Returns(p => "test");
Assert.Throws(typeof(InvalidOperationException), _presenter.Add());
}
[Test]
public void Add_EmptyId_ThrowException()
{
_view.CategoryId.Returns(p => "");
_view.CategoryName.Returns(p => "test");
Assert.Throws(typeof(InvalidOperationException), _presenter.Add());
}
[Test]
public void Add_SpaceOnlyId_ThrowException()
{
_view.CategoryId.Returns(p => " ");
_view.CategoryName.Returns(p => "test");
Assert.Throws(typeof(InvalidOperationException), _presenter.Add());
}
[Test]
public void Add_InvalidLowBoundId_ThrowException()
{
_view.CategoryId.Returns(p => "-1");
_view.CategoryName.Returns(p => "test");
Assert.Throws(typeof(InvalidOperationException), _presenter.Add());
}
[Test]
public void Add_InvalidHighBoundId_ThrowException()
{
_view.CategoryId.Returns(p => long.MaxValue.ToString() + "1");
_view.CategoryName.Returns(p => "test");
Assert.Throws(typeof(InvalidOperationException), _presenter.Add());
}
[Test]
public void Add_EmptyName_ThrowException()
{
_view.CategoryId.Returns(p => "1");
_view.CategoryName.Returns(p => "");
Assert.Throws(typeof(InvalidOperationException), _presenter.Add());
}
[Test]
public void Add_NullName_ThrowException()
{
_view.CategoryId.Returns(p => "1");
_view.CategoryName.Returns(p => null);
Assert.Throws(typeof(InvalidOperationException), _presenter.Add());
}
[Test]
public void Add_SpaceOnlyName_ThrowException()
{
_view.CategoryId.Returns(p => "1");
_view.CategoryName.Returns(p => " ");
Assert.Throws(typeof(InvalidOperationException), _presenter.Add());
}
[Test]
public void Add_NumberOnlyName_ThrowException()
{
_view.CategoryId.Returns(p => "1");
_view.CategoryName.Returns(p => "123");
Assert.Throws(typeof(InvalidOperationException), _presenter.Add());
}
我正确测试了吗?我的意思是这是测试类应该是什么样的?我错过了什么?这太多了吗?比如“你不需要测试空虚”或与我的测试或代码相关的任何其他问题?如果你在我的整个代码和/或架构中发现任何错误,我会感激你,如果你纠正我的话。谢谢!
更新: IView由.aspx页面继承。在代码背后我只需从点击事件内部调用presenter方法,通过按下按钮触发用户。至于记忆,我还没走那么远。只是坚持TDD。
答案 0 :(得分:1)
我将从应用程序层(演示者所在的位置)中删除验证逻辑,并将其提取到域层(存储库所在的位置)。
然后不要在演示者中进行验证,而是让演示者调用必要的验证器。
对于演示者的单元测试,您需要向演示者提供验证器模拟对象,并验证是否为数据调用了正确的验证方法。
所以你必须测试两件事: 1)测试演示者是否使用视图中的数据调用验证器 2)自己测试验证器
测试可能如下所示:
对于演示者(类CategoryPresenterTests):
[Test]
public void Add_CallsTheValidatorWithDataFromTheView()
{
_viewMock.CategoryId.Returns(p => "id");
_viewMock.CategoryName.Returns(p => "name");
_presenter.Add();
_categoryValidatorMock.Verify(x=>x.Validate("id", "name"), Times.Once);
}
[Test]
public void Add_ForwardsValidationExceptions()
{
_viewMock.CategoryId.Returns(p => "id");
_viewMock.CategoryName.Returns(p => "name");
_categoryValidatorMock.Setup(x=>x.Validate(...)).Throws<ValidationException>();
Assert.Throws<ValidationException>(() => _presenter.Add());
}
请注意,我们不关心视图中的具体输入,只是从视图中使用此确切数据调用验证器,并且传回结果(在这种情况下为异常或无异常)。
对于验证器(类CategoryValidatorTests。基本上所有当前的测试都在这里):
[Test]
public void NullId_ThrowsException() {
string id = null;
string name = "test";
Assert.Throws<ValidationException>(() => _validator.Validate(id, name));
}
注意我不知道NSubstitutes的语法,所以上面是伪代码..希望你能解读它:)
除此之外,我不会在演示者中创建存储库,而是通过构造函数注入他们的接口(就像你使用IView一样)。然后提供模拟对象,与验证器一样,验证演示者是否正确调用它们。
以上所有内容都应该允许您在演示者之外重用验证逻辑,这将使演示者远离一些复杂性,使他们能够更多地关注在模型和视图之间进行调解以及处理工作流程的实际目的。