C# 使用Ninject通过模拟和依赖注入进行单元测试

C# 使用Ninject通过模拟和依赖注入进行单元测试,c#,unit-testing,dependency-injection,ninject,moq,C#,Unit Testing,Dependency Injection,Ninject,Moq,我有下面的场景,到目前为止我还没有在使用Ninject时遇到过。我有以下的类结构(简化为便于阅读:)。首先是所有i文档的抽象基类 public abstract class DocumentController : IDocumentController, IDisposable { ... private IMessageBoxService messageBoxService; private IUndoRedoManager undoRedoManager;

我有下面的场景,到目前为止我还没有在使用Ninject时遇到过。我有以下的类结构(简化为便于阅读:)。首先是所有
i文档的抽象基类

public abstract class DocumentController : IDocumentController, IDisposable
{
    ...
    private IMessageBoxService messageBoxService;
    private IUndoRedoManager undoRedoManager;
    private FileSystemObserver observer;
    private bool fileChanging;

    public DocumentController(IMessageBoxService messageBoxService)
    {
        if (messageBoxService == null)
            throw new ArgumentNullException("messageBoxService");

        this.messageBoxService = messageBoxService;
    }
    ...
    private void FileChangedHandler(string p)
    {
        if (!fileChanging && p.CompareNoCase(FilePath))
        {
            fileChanging = true;
            try
            {
                (View as Form).UIThread(() =>
                {
                    DialogResult result = messageBoxService.DisplayMessage(
                        (Form)View,
                        String.Format(
                            MessageStrings.DocumentController_DocumentChanged,
                            FilePath,
                            Constants.Trademark),
                        CaptionStrings.ChangeNotification,
                        MessageBoxButtons.YesNo, MessageBoxIcon.Information);

                    if (result == DialogResult.Yes)
                    {
                        if (TryClose(new FormClosingEventArgs(CloseReason.None, false)))
                        {
                            MediatorService.Instance.NotifyColleagues(
                                MediatorService.Messages.OpenDocuments,
                                new List<string>() { FilePath });
                            Dispose();
                        }
                    }
                });
            }
            finally
            {
                fileChanging = false;
            }
        }
    }
    // Omitted code for not saved prompt for brevity.
}
现在,这在使用应用程序时非常有效。我遇到的问题是单元测试。我想做的是编写一个单元测试,打开我的文本文档,进行一些更改,然后在它们“脏”/“未保存”时尝试关闭它们。这将显示一个消息框,但我想模拟
IMessageBoxService
,并测试消息框是否确实在正确的条件下显示。我有下面的单元测试类

[TestClass]
public class DocumentManagementTests
{
    private IDocumentProvider documentProvider;
    private Moq.Mock<IProgress<string>> mockProgress;
    private List<string> filePaths = new List<string>();

    #region Initialization.
    [TestInitialize]
    public void Initialize()
    {
        documentProvider = CompositionRoot.Resolve<IDocumentProvider>();
        if (documentProvider == null)
            throw new ArgumentNullException("documentManager");

        mockProgress = new Mock<IProgress<string>>();
        mockProgress.Setup(m => m.Report(It.IsAny<string>()))
             .Callback((string s) =>
             {
                 Trace.WriteLine(s);
             });

        LoadTextDocumentFilePaths();
    }

    private void LoadTextDocumentFilePaths()
    {
        // Add two real file paths to filePaths.
        Assert.AreEqual(2, filePaths.Count);
    }

    [TestMethod]
    public void DocumentChangedSavePromptTest()
    {
        // HOW DO I INJECT A MOCKED IMessageBoxService to `IDocumentController`?

        // Open the documents internally.
        var documents = documentProvider.GetDocumentViews(filePaths, mockProgress.Object);
        Assert.AreEqual(2, documentProvider.DocumentControllerCache
            .Count(d => d.GetType() == typeof(TextEditorController)));

        // Ammend the text internally. 
        foreach (var d in documentProvider.DocumentControllerCache)
        {
            var controller = d.Key as TextEditorController;

        }
        // Need to finish writing the test.
    }
}

这是基于下面斯蒂芬·罗斯的评论。是否有任何理由不应该创建具体的
textededitorcontroller
s?

看起来您正在尝试将集成测试与单元测试混合在一起。您需要将模拟的
IMessageBoxService
注入到您的
CompositionRoot
中,以代替实际的
IMessageBoxService
。否则,在测试中预先创建
textEditorController
所需的所有内容,而不必使用
CompositionRoot
,我希望您使用此选项,而不必使用实际的
CompositionRoot
。您能澄清一下“集成测试”在这种情况下的含义吗?另外,您是否建议我创建一个具体版本的
TextEditorController
,并在构造函数中使用mock对象?我的意思是,您将
CompositionRoot
之类的对象与单元测试混合在一起,因此看起来好像您想要测试模块之间的交互(集成测试),但是,接下来讨论模拟与被测系统的依赖关系(更接近于单元测试)。如果你想做一个简单的单元测试,是的,我会说在调用构造函数之前创建一个具体的
textededitorcontroller
并模拟它的依赖关系。谢谢,这非常有帮助。在测试类中,我通过
[AssemblyInitialize]公共静态void Initialize(TestContext TestContext){CompositionRoot.Initialize(new DependencyModule());}
为所有测试类构建我的
CompositionRoot
,因此也许我应该在测试初始化中使用ctor注入。我只是担心创建一个类的具体实现不利于DI,还有一种更好更“合适”的方法。。。非常感谢您的时间。我过去和您一样认为依赖注入意味着拥有一个IOC容器,如Autofac等。但实际上,它只是意味着客户端委托外部代码来提供其依赖项,而不是创建自己的服务。创建一个具体的实现根本不反对DI。您的测试看起来更好,但我可能要评论的唯一一件事是您在一个测试用例中进行了多少测试,这不是问题,但在以后添加功能时可能会导致问题。看起来您正在尝试将集成测试与单元测试混合。您需要将模拟的
IMessageBoxService
注入到您的
CompositionRoot
中,以代替实际的
IMessageBoxService
。否则,在测试中预先创建
textEditorController
所需的所有内容,而不必使用
CompositionRoot
,我希望您使用此选项,而不必使用实际的
CompositionRoot
。您能澄清一下“集成测试”在这种情况下的含义吗?另外,您是否建议我创建一个具体版本的
TextEditorController
,并在构造函数中使用mock对象?我的意思是,您将
CompositionRoot
之类的对象与单元测试混合在一起,因此看起来好像您想要测试模块之间的交互(集成测试),但是,接下来讨论模拟与被测系统的依赖关系(更接近于单元测试)。如果你想做一个简单的单元测试,是的,我会说在调用构造函数之前创建一个具体的
textededitorcontroller
并模拟它的依赖关系。谢谢,这非常有帮助。在测试类中,我通过
[AssemblyInitialize]公共静态void Initialize(TestContext TestContext){CompositionRoot.Initialize(new DependencyModule());}
为所有测试类构建我的
CompositionRoot
,因此也许我应该在测试初始化中使用ctor注入。我只是担心创建一个类的具体实现不利于DI,还有一种更好更“合适”的方法。。。非常感谢您的时间。我过去和您一样认为依赖注入意味着拥有一个IOC容器,如Autofac等。但实际上,它只是意味着客户端委托外部代码来提供其依赖项,而不是创建自己的服务。创建一个具体的实现根本不反对DI。您的测试看起来更好,但我可能要评论的唯一一件事是您在一个测试用例中进行了多少测试,这不是问题,但在以后添加功能时可能会导致问题。
[TestClass]
public class DocumentManagementTests
{
    private IDocumentProvider documentProvider;
    private Moq.Mock<IProgress<string>> mockProgress;
    private List<string> filePaths = new List<string>();

    #region Initialization.
    [TestInitialize]
    public void Initialize()
    {
        documentProvider = CompositionRoot.Resolve<IDocumentProvider>();
        if (documentProvider == null)
            throw new ArgumentNullException("documentManager");

        mockProgress = new Mock<IProgress<string>>();
        mockProgress.Setup(m => m.Report(It.IsAny<string>()))
             .Callback((string s) =>
             {
                 Trace.WriteLine(s);
             });

        LoadTextDocumentFilePaths();
    }

    private void LoadTextDocumentFilePaths()
    {
        // Add two real file paths to filePaths.
        Assert.AreEqual(2, filePaths.Count);
    }

    [TestMethod]
    public void DocumentChangedSavePromptTest()
    {
        // HOW DO I INJECT A MOCKED IMessageBoxService to `IDocumentController`?

        // Open the documents internally.
        var documents = documentProvider.GetDocumentViews(filePaths, mockProgress.Object);
        Assert.AreEqual(2, documentProvider.DocumentControllerCache
            .Count(d => d.GetType() == typeof(TextEditorController)));

        // Ammend the text internally. 
        foreach (var d in documentProvider.DocumentControllerCache)
        {
            var controller = d.Key as TextEditorController;

        }
        // Need to finish writing the test.
    }
}
[TestClass]
public class DocumentManagementTests
{

    private Mock<IProgress<string>> mockProgress;
    private Mock<IMessageBoxService> mockMessageBoxService;

    private int messageBoxInvocationCounter = 0;
    private List<string> filePaths = new List<string>();

    [TestInitialize]
    public void Initialize()
    {
        // Progress mock.
        mockProgress = new Mock<IProgress<string>>();
        mockProgress.Setup(m => m.Report(It.IsAny<string>()))
             .Callback((string s) =>
             {
                 Trace.WriteLine(s);
             });
        Assert.IsNotNull(mockProgress);

        // Load the file paths.
        LoadTextDocumentFilePaths();
    }

    private void LoadTextDocumentFilePaths()
    {
        string path = String.Empty;
        string directory = Utils.GetAssemblyDirectory();
        foreach (var fileName in new List<string>() { "DocumentA.txt", "DocumentB.txt" })
        {
            path = Path.Combine(directory, "Resources", "TextEditorDocs", fileName);
            if (!File.Exists(path))
                throw new IOException($"{path} does not exist");

            filePaths.Add(path);
        }
        Assert.AreEqual(2, filePaths.Count);
    }

    [TestMethod]
    public void DocumentChangedSavePrompt_OnUserClosing_Test()
    {
        // Message box service mock.
        mockMessageBoxService = new Mock<IMessageBoxService>();
        mockMessageBoxService
            .Setup(m => m.DisplayMessage(It.IsAny<IWin32Window>(), It.IsAny<string>(), It.IsAny<string>(),
                It.IsAny<MessageBoxButtons>(), It.IsAny<MessageBoxIcon>(), It.IsAny<MessageBoxDefaultButton>()))
            .Returns((IWin32Window owner, string text,
                string caption, MessageBoxButtons buttons, MessageBoxIcon icon,
                MessageBoxDefaultButton defaultButton) => DialogResult.Cancel)
            .Callback((IWin32Window owner, string text,
                string caption, MessageBoxButtons buttons, MessageBoxIcon icon,
                MessageBoxDefaultButton defaultButton) =>
            {
                Trace.WriteLine("MockMessageBoxService Invoked");
                messageBoxInvocationCounter++;
            });
        Assert.IsNotNull(mockMessageBoxService);

        // Open the documents.
        List<TextEditorController> controllers = new List<TextEditorController>();
        foreach (var path in filePaths)
        {
            TextEditorController controller = new TextEditorController(
                CompositionRoot.Resolve<ITextEditorView>(),
                mockMessageBoxService.Object);
            Assert.IsNotNull(controller);

            controllers.Add(controller);
            TextEditorView view = (TextEditorView)controller.Open(path);

            view.TextEditor.Text += "*";
            Assert.IsTrue(controller.IsDirty);
        }

        // Test they are dirty and the message box is displayed.
        foreach (var controller in controllers)
        {
            bool didClose = controller.TryClose(
                new FormClosingEventArgs(CloseReason.UserClosing, false));
            Assert.IsFalse(didClose);
        }
        Assert.AreEqual(2, messageBoxInvocationCounter);
    }
}