在我的WPF应用程序中,我创建了两个独立的项目:UI(使用XAML和ViewModels)和“Core”(它具有人们称之为“域对象”或“业务对象”的对象 - 表示离散概念的对象在我的用例中)。我知道这是一个很好的做法。
但是我的许多业务对象都与多个数据源进行交互。例如,Document
对象可能包含数据库和文件中的数据。
然后,你可以用Document
“做”的一些事情 - 我在Document
上实现的方法 - 涉及其他资源。例如,Document.SubmitForProcessing()
调用Web服务。
所以我写了这个:
public class Document
{
public string Name { get; set; }
public string FilePath { get; set; }
public string FileData { get; set; }
private Document() { }
public static Document GetByID(int documentID, string databaseConnectionString, string baseFilePath)
{
// Using Dapper
Document newDoc = db.Query<Document>("SELECT Name, FilePath FROM Documents WHERE ID = @pID", new { pID = documentID });
newDoc.FileData = File.ReadAllText(Path.Combine(basePath, newDoc.FilePath));
return newDoc;
}
public void SubmitForProcessing(IWebService webService)
{
webService.ExecuteFoo(this.Name, this.FileData);
}
public void DoBusinessStuff()
{
this.FileData = this.FileData.Replace("foo", "bar");
}
}
毋庸置疑,这非常令人讨厌,而且编写测试也很困难。
所以我读到了依赖注入和存储库模式。但我不确定如何在这种情况下正确地做到这一点。我是否为每个数据源都有单独的存储库类,然后是某种DocumentFactory
或某些访问单独存储库并将Document对象拼接在一起的东西?或者有更简单的方法吗?
我主要关注的是使代码测试友好,这样我就可以编写一些单元测试而不需要模拟整个数据库和文件系统,而且还可以停止将每个参数的大杂烩传递给我拥有的每个工厂方法(例如GetByID(int documentID, string databaseConnectionString, string baseFilePath)
- 我的现实生活中有超过六个这样的参数)。
在类似的问题上,答案谈论的事情,如SOLID,YAGNI,存储库用于CRUD等。我重视这些原则,但我无法从中获得实用的设计。例如,Web服务实际上不是CRUD-y。我有一个“存储库”,以便我可以在单元测试期间将其切换出来吗?文件系统怎么样?
TL; DR - 该代码出了什么问题?
指导赞赏。谢谢!
答案 0 :(得分:0)
如果不使用某些高级单元测试工具(例如Stubs:https://msdn.microsoft.com/en-us/library/ff798446.aspx),静态方法和I / O函数很难测试。您遇到的问题是您有一个静态方法调用两个I / O函数。目标是将它们分离。
我要做的第一件事是将GetById方法重构为Factory类。您可以通过将数据库和文件系统I / O实现作为接口传递来创建工厂类。 使用接口的优点是它允许您模拟I / O行为。对于像我下面所做的简单接口,我甚至可以在我的测试代码中简单地实现它们而不需要任何嘲弄。通过这种方式,您可以将GetById方法的业务逻辑与Factory类隔离开来,并且您不会费心测试I / O本身,因为它是由数据库提供程序和win32 api完成的。这就是你的全部需求。
class Document
{
public string FileData { get; set; }
public string FileRelativePath { get; set; }
}
interface IDocumentRepository
{
Document Get(int id);
}
abstract class DocumentFactory
{
public abstract Document Create(int docId);
}
interface IFileStore
{
string Read(string fileName);
}
class ConcreteDocumentFactory : DocumentFactory
{
private IDocumentRepository _db;
private IFileStore _fileStore;
public ConcreteDocumentFactory(IDocumentRepository db, IFileStore fileStore)
{
_db = db;
_fileStore = fileStore;
}
public override Document Create(int docId)
{
Document newDoc = _db.Get(docId);
newDoc.FileData = _fileStore.Read(newDoc.FileRelativePath);
return newDoc;
}
}
/////// Test Code Below
[TestFixture]
class TestClass
{
class TestFriendlyFileStore : IFileStore
{
public string Read(string fileName)
{
if (fileName == "sample.txt")
return "Some File Content";
throw new Exception("Not good file name.");
}
}
class TestFriendlyDocRepo : IDocumentRepository
{
public Document Get(int id)
{
if (id != 999)
return new Document() {FileRelativePath = "sample.txt"};
throw new Exception("Not good id.");
}
}
[Test]
public void Test()
{
var concreteDocFactory = new ConcreteDocumentFactory(new TestFriendlyDocRepo(), new TestFriendlyFileStore());
var doc = concreteDocFactory.Create(999);
Assert.AreEqual(doc.FileData == "Some File Content")
}
}
答案 1 :(得分:0)
如上一个答案中所述,仅适用于测试您的业务类,并且仅在客户端代码上仅捕获来自基础架构,数据库或Web服务调用的预期异常,因此我建议您设置与基础架构无关的软件让它具有SOLID和Testable:
public class Document : IAcceptDocumentVisitor
{
public int Id { get; private set; }
public string Name { get; private set; }
public string FilePath { get; private set; }
public string FileData { get; private set; }
public Document(int id, string name, string filePath, string fileData)
{
Id = id;
Name = name;
FilePath = filePath;
FileData = fileData;
}
/// <summary>
/// This method replace SubmitForProcessing
/// </summary>
/// <param name="visitor"></param>
public void Accept(IDocumentVisitor visitor)
{
if (visitor == null) throw new ArgumentNullException(nameof(visitor));
visitor.Visit(Name, FileData);
}
public void ReplaceFileData(string fileData, Action onSuccess)
{
//Business valdation
var validate = true;
//Business valdation
if (!validate) return;
FileData = fileData;
onSuccess();
}
}
public interface IAcceptDocumentVisitor
{
void Accept(IDocumentVisitor visitor);
}
public interface IDocumentVisitor
{
void Visit(string name, string fileData);
}
public class FakeWebServiceVisitor : IDocumentVisitor
{
public void Visit(string name, string fileData)
{
Name = name;
FileData = fileData;
}
public string FileData { get; set; }
public string Name { get; set; }
}
public class WebServiceVisitor : IDocumentVisitor
{
public void Visit(string name, string fileData)
{
//Call web service
//webService.ExecuteFoo(this.Name, this.FileData);
}
}
public interface IDocumentReader
{
Document GetById(int id);
}
public class DocumentDbReader : IDocumentReader
{
public Document GetById(int id)
{
//Get from database
//Document newDoc = db.Query<Document>("SELECT Name, FilePath FROM Documents WHERE ID = @pID", new { pID = documentID });
return new Document(id, "Name", "Path", "Data");
}
}
使用一些OOP技术和模式,例如存储库,访问者,CQRS和实体,您可以获得诸如SOLID代码之类的好处,并且您也将开始使代码可测试:
[TestClass]
public class DocumentSpecs
{
public string Name = "Name";
public string FilePath = "Path";
public string FileData = "Data";
[TestMethod]
public void AcceptVisitorCorrectly()
{
//Arrange
var document = new DocumentDbReader().GetById(0);
var visitor = new FakeWebServiceVisitor();
//Act
document.Accept(visitor);
//Assert
Assert.AreEqual(FileData, visitor.FileData);
Assert.AreEqual(Name, visitor.Name);
}
[TestMethod]
public void ReplaceFileDataCorrectly()
{
//Arrange
var successActionCalled = false;
var expectedFileData = Guid.NewGuid().ToString();
var document = new DocumentDbReader().GetById(0);
//Act
var documentInitialData = document.FileData;
document.ReplaceFileData(expectedFileData, () => successActionCalled = true);
//Assert
Assert.IsTrue(successActionCalled);
Assert.AreEqual(expectedFileData, document.FileData);
Assert.IsFalse(documentInitialData == document.FileData);
}
}
结果如下:
亲切的问候!