Channels ▼
RSS

Design

Testing Complex Systems


These two interfaces isolate the domain from the rest of the application and allow mocking the domain for testing purposes, as you'll soon see. The user interface state and logic are managed by the Presenter class (See Listing Two). The Presenter accepts in its constructor an IMainWindow and IMindReader reference, and it implements the IMainWindowEvents and IMindReaderEvents interface.

Listing Two

namespace MindReader
{
    /// <summary>
    /// In charge of MainWindow
    /// </summary>
    public class Presenter : 
        IMainWindowEvents,
        IMindReaderEvents
    {
        IMainWindow _window;
        IMindReader _reader;

        public Presenter(IMainWindow window, IMindReader reader)
        {
            _window = window;
            _reader = reader;
        }

        public void OnGoButtonClick()
        {
            _window.EnableGoButton(false);
            _window.SetThoughtText("You are thinking of...");
            _reader.Read();            
        }

        public void OnKey(string key)
        {
        }

        public void OnClose()
        {
        }

        public void OnProgress(int percent)
        {
            _window.UpdteProgressBar(percent);
        }

        public void OnReadComplete(string thought)
        {
            OnProgress(100);
            _window.SetThoughtText(thought);
            _window.EnableGoButton(true);
        }
    }
}

The Presenter is responsible for the following UI state:

  1. When the go button is pressed, it should start a mind reading session, reset the progress bar to 0, reset the text box to the generic message: "You are thinking of…" and disable the button.
  2. When progress reports arrive via OnProgress(), it should update the progress bar.
  3. When OnReadComplete() is called, it should display the new thought, update progress to 100%, and enable the go button.

The PresenterTest verifies this entire sequence with the help of two mock objects in a few lines:

        [TestMethod]
        public void Test()
        {
            _presenter.OnGoButtonClick();

            // The presenter should first disable the button and in the end re-enable it.
            CollectionAssert.AreEqual(new List<bool>() { false, true }, _mockWindow.EnableGoButtonCalls);

            // The presenter should set the thought twice, first to the generic message and then to the test thought
            CollectionAssert.AreEqual(new List<string>() { "You are thinking of...", "whatever" }, _mockWindow.SetThoughtCalls);
            
            // The presenter should update the progress bar 5 times (initially to 0, finally to 100)
            CollectionAssert.AreEqual(new List<int>() { 0, 25, 50, 75, 100 }, _mockWindow.UpdateProgressCalls);
        }

When I ran this test, I discovered a bug — I forgot to reset the progress bar to 0 in the Presenter. This is exactly the type of bug that may slip through the cracks and get discovered only in production.

Here is the test setup that hooks up the real Presenter to the mock window and the mock mind reader:

 [TestInitialize]
        public void Setup()
        {            
            _mockWindow = new MockWindow();
            _mockMindReader = new MockMindReader(new int[] { 25, 50, 75 }, "whatever");
            _presenter = new Presenter(_mockWindow, _mockMindReader);
            _mockMindReader.AttachPresenter(_presenter);
        }

The test itself simulates the button by calling the Presenter's OnGoButtonClick() method (from IMainWindowEvents).

The MockWindow (see Listing Three) implements the IMainWindow interface, but all it does is record the calls to its methods.

Listing Three

using MindReader;
using System.Collections.Generic;
namespace MindReaderTest
{
    public class MockWindow : IMainWindow
    {
        public List<bool> EnableGoButtonCalls = new List<bool>();
        public List<string> SetThoughtCalls = new List<string>();
        public List<int> UpdateProgressCalls = new List<int>();
        
        public void EnableGoButton(bool enable)
        {
            EnableGoButtonCalls.Add(enable);
        }

        public void SetThoughtText(string thought)
        {
            SetThoughtCalls.Add(thought);
        }

        public void UpdteProgressBar(int percent)
        {
            UpdateProgressCalls.Add(percent);
        }
    }
}

The MockMindReader (see Listing Four) implements the IMainReader interface.

Listing Four

using System;
using MindReader;
using System.Collections.Generic;

namespace MindReaderTest
{
    public class MockMindReader : IMindReader

    {
        int[] _progressReports;
        string _thought;
        Presenter _presenter;

        public MockMindReader(int[] progressReports, string thought)
        {
            _progressReports = progressReports;
            _thought = thought;
        }

        public void AttachPresenter(Presenter presenter)
        {
            _presenter = presenter;
        }

        public void Read()
        {
            foreach (int progress in _progressReports)
            {
                _presenter.OnProgress(progress);
            }

            _presenter.OnReadComplete(_thought);
        }
    }
}

The MockMindReader also provides a canned mind reading session by accepting in its constructor a list of progress reports and a hard-coded thought:

        public MockMindReader(int[] progressReports, string thought)
        {
            _progressReports = progressReports;
            _thought = thought;
        }

Later, the test attaches the Presenter to it as a sink. When the Presenter calls the Read() method (in response to the simulated button click), the mock mind reader sends its canned progress reports via OnProgress() and finally calls OnReadComplete(). The Presenter calls the appropriate methods on the mock main window, which records everything. Finally, the test verifies that the correct methods were invoked by checking the mock window's recorded calls.

This whole setup took very little time to write. It allows full testing of the UI management state, without dealing with an actual hard-to-test UI, and can be extended or modified easily when the real UI changes (and it will, count on it).

The steps I applied to enable testing transcend .NET, WPF, and the user interface:

  • Access all dependencies via interfaces (IMainWindow, IThoughtReader).
  • Mock dependencies with simple mock objects that capture the state and flow you want to test.
  • Hook up the object/system under test to all its mocked dependencies.
  • Run the object/system through the test scenarios.
  • Verify the object/system behaves as expected.

Testing Networking Code

Networking has become pervasive. Today, people often invoke a Web service, where they used to add a library to their application or, god forbid, write some code themselves. Designing a safe, robust, and performant connected system is much more complicated than a standalone system. The reason is that you have to handle many more failure modes and a lot of stuff that is not related to your code functionality, such as security and authentication. The remote system might disappear, slow down, or change at any moment, including in the middle of sending data or halfway through a complicated handshake. The reverse is true if your system is the remote service. How do you support old clients? How do you make sure your system scales and is highly available? How do you make sure attackers don't steal your data or bring your system down? These are important concerns and require a lot of engineering. To make sure you've got it right, you have to be able to simulate and test all these scenarios.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video