我有以下辅助方法,它从 DbEntityValidationException 中取出验证消息。我们需要这个,因为默认情况下验证的详细信息不会添加到异常中。
public static string LogMessageDbEntityValidationException(DbEntityValidationException ex)
{
StringBuilder error = new StringBuilder();
error.AppendLine("Validation Error details for DbEntityValidationException throw: ");
foreach (var validationErrors in ex.EntityValidationErrors)
{
foreach (var validationError in validationErrors.ValidationErrors)
{
error.AppendLine(string.Format("Property: {0} , Error: {1}",
validationError.PropertyName, validationError.ErrorMessage));
}
}
return error.ToString();
}
我在尝试创建单元测试时遇到了问题,特别是我无法创建 DbEntityValidationResult,因为它需要 DbEntityEntry 的实例,而该实例没有公共构造函数。
这是损坏的单元测试,创建 DbEntityEntry 失败:
public void LogMessageDbEntityValidationExceptionTest()
{
string errorMessage = "Unit Test Error Message";
string expected = "Not valid data.";
List<DbEntityValidationResult> entityValidationResults = new List<DbEntityValidationResult>();
List<DbValidationError> errorList = new List<DbValidationError>();
DbEntityValidationException ex;
errorList.Add(new DbValidationError("TestProperty", expected));
entityValidationResults.Add(new DbEntityValidationResult(new System.Data.Entity.Infrastructure.DbEntityEntry(), errorList));
ex = new DbEntityValidationException(errorMessage, entityValidationResults);
string actual = Common.LogMessageDbEntityValidationException(ex);
Assert.IsTrue(actual.Contains(expected));
}
注意,DbEntityEntry 没有实现接口,所以我不能使用模拟/伪造。
一些模拟框架(例如 Moq)可以进行方法重定向,允许您模拟没有接口的类。其做法类似于 http://www.codenutz.com/unit-testing-mocking-base-class-methods-with-moq/
话虽如此,这不是一个好的编程实践,因为您将使您的测试代码永远依赖于该框架,并且您会失去单元测试的一些设计优势。
你可以做的是编写一个代理类来包装你的不可测试的对象并在其上添加一个接口,然后你每次使用普通类时都使用代理类。
一个可能的解决方案可能是将异常包装到您可以控制的内容中:
public interface IValidationErrorContainer
{
IEnumerable<DbValidationError> ValidationErrors { get; }
}
public class ValidationErrorContainer : IValidationErrorContainer
{
private readonly DbEntityValidationException _exception;
public ValidationErrorContainer(DbEntityValidationException exception)
{
_exception = exception;
}
public IEnumerable<DbValidationError> ValidationErrors
=> _exception.EntityValidationErrors.SelectMany(validationError => validationError.ValidationErrors);
}
通过使用该接口,您可以创建一个测试替身并将其传递给您的日志记录方法:
public string ComposeValidationErrors(IValidationErrorContainer container)
{
var error = new StringBuilder(
"Validation Error details for DbEntityValidationException throw:");
foreach (var validationErrors in container.ValidationErrors)
{
error.AppendFormat(
"\nProperty: {0}, Error: {1}",
validationError.PropertyName,
validationError.ErrorMessage);
}
return error.ToString();
}
现在您可以在这样的测试中使用它:
public void ComposeValidationErrors_ReturnsTextContainingExpectedSubstring()
{
var expected = "Not valid data.";
var container = new FakeValidationErrorContainer
{
ValidationErrors = new[] { new DbValidationError("TestProperty", expected) }
};
var actual = Common.ComposeValidationErrors(container);
Assert.That(actual, Contains.Substring(expected));
}
private class FakeValidationErrorContainer : IValidationErrorContainer
{
public IEnumerable<DbValidationError> ValidationErrors { get; set; }
}
当然,这个解决方案仅测试验证错误是否组成了您期望的字符串。 为了测试
ValidationErrorContainer
的行为,我使用集成测试来创建内存数据库并触发预期的异常。
注意:我对代码的命名和结构采取了一些自由:
ComposeValidationErrors
,因为它实际上并不记录消息StringBuilder
使用 AppendFormat
而不是 string.Format
以提高可读性Assert.That
和 Contains.Substring
约束以提高可读性请勿测试此方法。其实是没有任何逻辑的。它也不是某种业务领域方法。不要测试它。您不必拥有 100% 的测试覆盖率。这是非常糟糕的做法。