5
\$\begingroup\$

I have the following scenario that I have not come across so far using Ninject. I have the following class structure (simplified for easy of reading :]). First the abstract base class for all IDocuments

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.
}

This class watches all documents that are open and if they are changed in the file system shows a prompt. If a close is attempted whilst they are in a "dirty" state, the same IMessageBoxService prompts to save.

An example of the derived concrete class ctors is

public class TextEditorController : DocumentController, IZoom
{
    ...
    public TextEditorController(ITextEditorView view, IMessageBoxService messageBoxService) 
        : base(messageBoxService)
    {
        if (view == null)
            throw new ArgumentNullException("view");

        this.view = view;
        InitializeEventHandlers();
    }
    ...
}

Now this works great when using the application. The problem I have is with unit testing. What I want to do is write a unit test that opens my text documents, makes some changes and then trys to close them when they are "dirty"/unsaved. This will show a message box, but I want to Mock the IMessageBoxService and test whether the message box is indeed displayed under the right conditions. I have the following unit test class in which I have reverted to instantiating concrete implementations of things; it works but feels bad...

[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()
    {
        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)
        {
            // CONCRETE IMPLEMENTATIONS - BAD!??
            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);
    }
}

I want to do something like this

[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.
    }
}

So I want to know:

  1. How I can inject a mocked IMessageBoxService into the ctor for the IDocumentController class?
  2. Is the above is not possible, is there a way I can use DI in my test, if so how?
  3. Is what I have done reasonable in the worst case scenario?

Thanks for your time.

\$\endgroup\$
  • 2
    \$\begingroup\$ Hello! Please don't make changes to your original question once it has been reviewed, as that invalidates the current answers. See our meta site on performing iterative reviews for more information! \$\endgroup\$ – syb0rg Jun 9 '16 at 12:40
  • \$\begingroup\$ Please see What to do when someone answers. \$\endgroup\$ – Mast Jun 9 '16 at 12:41
  • \$\begingroup\$ Thanks, I was not aware it was reasonable and valid to ask another question based on a previous one... \$\endgroup\$ – MoonKnight Jun 9 '16 at 12:45
7
\$\begingroup\$

Here be dragons :P

This is a conceptual minefield.

I have done this exact thing and can relate, you instinctively want to test a lifecycle.

a start-to-finish stub implementation.....

but don't.


the golden rule of unit testing is never test an interface. test an implementation.

I'd add a second rule personally, don't test module interaction.

If you have

       A->activates->B-> returns a C

if A fires when called correctly ...good.

if B activates (regardless of who called it) ... good;

if B returns an expected IC ...good.

does it MATTER if A->b->c evaluates properly?


infact i would suggest not only does it not. but if you require it does, your individual components are designed wrong.

you have no reason to test Ninject.

that is like sending yourself a letter to check the formatting and quizzing the postman.

if you want to test an abstract class, try not to. why did you MAKE the abstract class? to be used in an implementation.

test the implementation

\$\endgroup\$
  • \$\begingroup\$ Thanks very much for this, most appreciated. I have implemented the class as per my edit. I appreciate the test method looks like it is doing a lot, but I am not sure I can cut it down in size. I hope this looks better! :] \$\endgroup\$ – MoonKnight Jun 9 '16 at 12:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.