8
\$\begingroup\$

The opening sentence of an answer I received in my previous post snowballed, and led to completely ditching the previous approach. Mocking my IDE with a MockFactory worked ok, ...for some values of "ok" - the more components needed to be involved, the messier the setup code was getting.

So instead of manufacturing mocks, I thought I'd build them. Enter the MockVbeBuilder API:

Mock<VBE> mock = new MockVbeBuilder()
    .ProjectBuilder("TestProject1", vbext_ProjectProtection.vbext_pp_none)
    .AddComponent("TestModule1", vbext_ComponentType.vbext_ct_StdModule, contentForProject1Module1)
    .AddComponent("TestModule2", vbext_ComponentType.vbext_ct_StdModule, contentForProject1Module2)
    .UserFormBuilder("UserForm1", codeBehindForProject1UserForm1)
    .AddControl("Button1")
    .AddControl("Button2")
    .MockProjectBuilder()
    .AddComponent("TestClass1", vbext_ComponentType.vbext_ct_ClassModule, contentForProject1Class1)
    .AddComponent("ThisWorkbook", vbext_ComponentType.vbext_ct_Document, contentForProject1ThisWorkbook)
    .MockVbeBuilder()
    .ProjectBuilder("TestProject2", vbext_ProjectProtection.vbext_pp_locked)
    .AddComponent("TestClass1", vbext_ComponentType.vbext_ct_ClassModule, contentForProject2Class1)
    .AddReference("TestProject1", "PathToProject1")
    .MockVbeBuilder()
    .Build();

I also exposed a "shortcut" builder, to mock the entire IDE out of just a single standard module:

VBComponent component;
Mock<VBE> mock = new MockVbeBuilder().BuildFromSingleStandardModule(content, out component);

This simplifies usage for the simpler tests that really only need a single code module with some test content.


Here's the MockVbeBuilder class:

namespace RubberduckTests.Mocks
{
    /// <summary>
    /// Builds a mock <see cref="VBE"/>.
    /// </summary>
    public class MockVbeBuilder
    {
        private readonly Mock<VBE> _vbe;

        private Mock<VBProjects> _vbProjects;
        private readonly ICollection<VBProject> _projects = new List<VBProject>();

        private Mock<CodePanes> _vbCodePanes;
        private readonly ICollection<CodePane> _codePanes = new List<CodePane>(); 

        public MockVbeBuilder()
        {
            _vbe = CreateVbeMock();
        }

        /// <summary>
        /// Adds a project to the mock VBE.
        /// Use a <see cref="MockProjectBuilder"/> to build the <see cref="project"/>.
        /// </summary>
        /// <param name="project">A mock <see cref="VBProject"/>.</param>
        /// <returns>Returns the <see cref="MockVbeBuilder"/> instance.</returns>
        public MockVbeBuilder AddProject(Mock<VBProject> project)
        {
            project.SetupGet(m => m.VBE).Returns(_vbe.Object);
            _projects.Add(project.Object);

            foreach (var component in _projects.SelectMany(vbProject => vbProject.VBComponents.Cast<VBComponent>()))
            {
                _codePanes.Add(component.CodeModule.CodePane);
            }

            return this;
        }

        /// <summary>
        /// Creates a <see cref="MockProjectBuilder"/> to build a new project.
        /// </summary>
        /// <param name="name">The name of the project to build.</param>
        /// <param name="protection">A value that indicates whether the project is protected.</param>
        public MockProjectBuilder ProjectBuilder(string name, vbext_ProjectProtection protection)
        {
            var result = new MockProjectBuilder(name, protection, () => _vbe.Object, this);
            return result;
        }

        /// <summary>
        /// Gets the mock <see cref="VBE"/> instance.
        /// </summary>
        public Mock<VBE> Build()
        {
            return _vbe;
        }

        /// <summary>
        /// Gets a mock <see cref="VBE"/> instance, 
        /// containing a single "TestProject1" <see cref="VBProject"/>
        /// and a single "TestModule1" <see cref="VBComponent"/>, with the specified <see cref="content"/>.
        /// </summary>
        /// <param name="content">The VBA code associated to the component.</param>
        /// <param name="component">The created <see cref="VBComponent"/></param>
        /// <returns></returns>
        public Mock<VBE> BuildFromSingleStandardModule(string content, out VBComponent component)
        {
            var builder = ProjectBuilder("TestProject1", vbext_ProjectProtection.vbext_pp_none);
            builder.AddComponent("TestModule1", vbext_ComponentType.vbext_ct_StdModule, content);
            var project = builder.Build();
            component = project.Object.VBComponents.Item(0);
            return AddProject(project).Build();
        }

        private Mock<VBE> CreateVbeMock()
        {
            var vbe = new Mock<VBE>();
            var windows = new MockWindowsCollection {VBE = vbe.Object};
            vbe.Setup(m => m.Windows).Returns(windows);
            vbe.SetupProperty(m => m.ActiveCodePane);
            vbe.SetupProperty(m => m.ActiveVBProject);

            vbe.SetupGet(m => m.SelectedVBComponent).Returns(() => vbe.Object.ActiveCodePane.CodeModule.Parent);
            vbe.SetupGet(m => m.ActiveWindow).Returns(() => vbe.Object.ActiveCodePane.Window);

            var mainWindow = new Mock<Window>();
            mainWindow.Setup(m => m.HWnd).Returns(0);

            vbe.SetupGet(m => m.MainWindow).Returns(mainWindow.Object);

            _vbProjects = CreateProjectsMock();
            vbe.SetupGet(m => m.VBProjects).Returns(() => _vbProjects.Object);

            _vbCodePanes = CreateCodePanesMock();
            vbe.SetupGet(m => m.CodePanes).Returns(() => _vbCodePanes.Object);

            return vbe;
        }

        private Mock<VBProjects> CreateProjectsMock()
        {
            var result = new Mock<VBProjects>();

            result.Setup(m => m.GetEnumerator()).Returns(_projects.GetEnumerator());
            result.As<IEnumerable>().Setup(m => m.GetEnumerator()).Returns(_projects.GetEnumerator());

            result.Setup(m => m.Item(It.IsAny<int>())).Returns<int>(value => _projects.ElementAt(value));
            result.SetupGet(m => m.Count).Returns(_projects.Count);


            return result;
        }

        private Mock<CodePanes> CreateCodePanesMock()
        {
            var result = new Mock<CodePanes>();

            result.Setup(m => m.GetEnumerator()).Returns(_codePanes.GetEnumerator());
            result.As<IEnumerable>().Setup(m => m.GetEnumerator()).Returns(_codePanes.GetEnumerator());

            result.Setup(m => m.Item(It.IsAny<int>())).Returns<int>(value => _codePanes.ElementAt(value));
            result.SetupGet(m => m.Count).Returns(_codePanes.Count);

            return result;
        }
    }
}

The MockProjectBuilder class:

namespace RubberduckTests.Mocks
{
    /// <summary>
    /// Builds a mock <see cref="VBProject"/>.
    /// </summary>
    public class MockProjectBuilder
    {
        private readonly Func<VBE> _getVbe;
        private readonly MockVbeBuilder _mockVbeBuilder;
        private readonly Mock<VBProject> _project;
        private readonly Mock<VBComponents> _vbComponents;
        private readonly Mock<References> _vbReferences;

        private readonly List<VBComponent> _components = new List<VBComponent>();
        private readonly List<Reference> _references = new List<Reference>(); 

        public MockProjectBuilder(string name, vbext_ProjectProtection protection, Func<VBE> getVbe, MockVbeBuilder mockVbeBuilder)
        {
            _getVbe = getVbe;
            _mockVbeBuilder = mockVbeBuilder;

            _project = CreateProjectMock(name, protection);

            _vbComponents = CreateComponentsMock();
            _project.SetupGet(m => m.VBComponents).Returns(_vbComponents.Object);

            _vbReferences = CreateReferencesMock();
            _project.SetupGet(m => m.References).Returns(_vbReferences.Object);
        }

        /// <summary>
        /// Adds a new component to the project.
        /// </summary>
        /// <param name="name">The name of the new component.</param>
        /// <param name="type">The type of component to create.</param>
        /// <param name="content">The VBA code associated to the component.</param>
        /// <returns>Returns the <see cref="MockProjectBuilder"/> instance.</returns>
        public MockProjectBuilder AddComponent(string name, vbext_ComponentType type, string content)
        {
            var component = CreateComponentMock(name, type, content);
            return AddComponent(component);
        }

        /// <summary>
        /// Adds a new mock component to the project.
        /// Use the <see cref="AddComponent(string,vbext_ComponentType,string)"/> overload to add module components.
        /// Use this overload to add user forms created with a <see cref="MockUserFormBuilder"/> instance.
        /// </summary>
        /// <param name="component">The component to add.</param>
        /// <returns>Returns the <see cref="MockProjectBuilder"/> instance.</returns>
        public MockProjectBuilder AddComponent(Mock<VBComponent> component)
        {
            _components.Add(component.Object);
            _getVbe().ActiveCodePane = component.Object.CodeModule.CodePane;
            return this;            
        }

        /// <summary>
        /// Adds a mock reference to the project.
        /// </summary>
        /// <param name="name">The name of the referenced library.</param>
        /// <param name="filePath">The path to the referenced library.</param>
        /// <returns>Returns the <see cref="MockProjectBuilder"/> instance.</returns>
        public MockProjectBuilder AddReference(string name, string filePath)
        {
            var reference = CreateReferenceMock(name, filePath);
            _references.Add(reference.Object);
            return this;
        }

        /// <summary>
        /// Builds the project, adds it to the VBE,
        /// and returns a <see cref="MockVbeBuilder"/>
        /// to continue adding projects to the VBE.
        /// </summary>
        /// <returns></returns>
        public MockVbeBuilder MockVbeBuilder()
        {
            _mockVbeBuilder.AddProject(Build());
            return _mockVbeBuilder;
        }

        /// <summary>
        /// Creates a <see cref="MockUserFormBuilder"/> to build a new form component.
        /// </summary>
        /// <param name="name">The name of the component.</param>
        /// <param name="content">The VBA code associated to the component.</param>
        public MockUserFormBuilder UserFormBuilder(string name, string content)
        {
            var component = CreateComponentMock(name, vbext_ComponentType.vbext_ct_MSForm, content);
            return new MockUserFormBuilder(component, this);
        }

        /// <summary>
        /// Gets the mock <see cref="VBProject"/> instance.
        /// </summary>
        public Mock<VBProject> Build()
        {
            return _project;
        }

        private Mock<VBProject> CreateProjectMock(string name, vbext_ProjectProtection protection)
        {
            var result = new Mock<VBProject>();

            result.SetupProperty(m => m.Name, name);
            result.SetupGet(m => m.Protection).Returns(() => protection);
            result.SetupGet(m => m.VBE).Returns(_getVbe);

            return result;
        }

        private Mock<VBComponents> CreateComponentsMock()
        {
            var result = new Mock<VBComponents>();

            result.SetupGet(m => m.Parent).Returns(() => _project.Object);
            result.SetupGet(m => m.VBE).Returns(_getVbe);

            result.Setup(c => c.GetEnumerator()).Returns(() => _components.GetEnumerator());
            result.As<IEnumerable>().Setup(c => c.GetEnumerator()).Returns(() => _components.GetEnumerator());

            result.Setup(m => m.Item(It.IsAny<int>())).Returns<int>(index => _components.ElementAt(index));
            result.Setup(m => m.Item(It.IsAny<string>())).Returns<string>(name => _components.Single(item => item.Name == name));
            result.SetupGet(m => m.Count).Returns(_components.Count);

            return result;
        }

        private Mock<References> CreateReferencesMock()
        {
            var result = new Mock<References>();

            result.SetupGet(m => m.Parent).Returns(() => _project.Object);
            result.SetupGet(m => m.VBE).Returns(_getVbe);

            result.Setup(m => m.GetEnumerator()).Returns(() => _references.GetEnumerator());
            result.As<IEnumerable>().Setup(m => m.GetEnumerator()).Returns(() => _references.GetEnumerator());

            result.Setup(m => m.Item(It.IsAny<int>())).Returns<int>(index => _references.ElementAt(index));
            result.SetupGet(m => m.Count).Returns(_references.Count);

            return result;
        }

        private Mock<Reference> CreateReferenceMock(string name, string filePath)
        {
            var result = new Mock<Reference>();

            result.SetupGet(m => m.VBE).Returns(_getVbe);
            result.SetupGet(m => m.Collection).Returns(() => _vbReferences.Object);

            result.SetupGet(m => m.Name).Returns(() => name);
            result.SetupGet(m => m.FullPath).Returns(() => filePath);

            return result;
        }

        private Mock<VBComponent> CreateComponentMock(string name, vbext_ComponentType type, string content)
        {
            var result = new Mock<VBComponent>();

            result.SetupGet(m => m.VBE).Returns(_getVbe);
            result.SetupGet(m => m.Collection).Returns(() => _vbComponents.Object);
            result.SetupGet(m => m.Type).Returns(() => type);
            result.SetupProperty(m => m.Name, name);

            var module = CreateCodeModuleMock(name, content);
            module.SetupGet(m => m.Parent).Returns(() => result.Object);
            result.SetupGet(m => m.CodeModule).Returns(() => module.Object);

            result.Setup(m => m.Activate());

            return result;
        }

        private Mock<CodeModule> CreateCodeModuleMock(string name, string content)
        {
            var codePane = CreateCodePaneMock(name);
            codePane.SetupGet(m => m.VBE).Returns(_getVbe);

            var result = CreateCodeModuleMock(content);
            result.SetupGet(m => m.VBE).Returns(_getVbe);
            result.SetupGet(m => m.CodePane).Returns(() => codePane.Object);

            codePane.SetupGet(m => m.CodeModule).Returns(() => result.Object);
            return result;
        }

        private Mock<CodeModule> CreateCodeModuleMock(string content)
        {
            var lines = content.Split(new[] { Environment.NewLine }, StringSplitOptions.None).ToList();

            var codeModule = new Mock<CodeModule>();
            codeModule.SetupGet(c => c.CountOfLines).Returns(() => lines.Count);

            // ReSharper disable once UseIndexedProperty
            codeModule.Setup(m => m.get_Lines(It.IsAny<int>(), It.IsAny<int>()))
                .Returns<int, int>((start, count) => String.Join(Environment.NewLine, lines.Skip(start - 1).Take(count)));

            codeModule.Setup(m => m.ReplaceLine(It.IsAny<int>(), It.IsAny<string>()))
                .Callback<int, string>((index, str) => lines[index - 1] = str);

            codeModule.Setup(m => m.DeleteLines(It.IsAny<int>(), It.IsAny<int>()))
                .Callback<int, int>((index, count) => lines.RemoveRange(index - 1, count));

            codeModule.Setup(m => m.InsertLines(It.IsAny<int>(), It.IsAny<string>()))
                .Callback<int, string>((index, newLine) => lines.Insert(index - 1, newLine));

            return codeModule;
        }

        private Mock<CodePane> CreateCodePaneMock(string name)
        {
            var windows = _getVbe().Windows as MockWindowsCollection;
            if (windows == null)
            {
                throw new InvalidOperationException("VBE.Windows collection must be a MockWindowsCollection object.");
            }

            var codePane = new Mock<CodePane>();
            var window = windows.CreateWindow(name);
            windows.Add(window);

            codePane.Setup(p => p.SetSelection(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()));
            codePane.Setup(p => p.Show());

            codePane.SetupGet(p => p.VBE).Returns(_getVbe);
            codePane.SetupGet(p => p.Window).Returns(() => window);

            return codePane;
        }
    }
}

And the MockUserFormBuilder class:

namespace RubberduckTests.Mocks
{
    /// <summary>
    /// Builds a mock <see cref="UserForm"/> component.
    /// </summary>
    public class MockUserFormBuilder
    {
        private readonly Mock<VBComponent> _component;
        private readonly MockProjectBuilder _mockProjectBuilder;
        private readonly Mock<Controls> _vbControls;
        private readonly ICollection<Mock<Control>> _controls = new List<Mock<Control>>();

        public MockUserFormBuilder(Mock<VBComponent> component, MockProjectBuilder mockProjectBuilder)
        {
            if (component.Object.Type != vbext_ComponentType.vbext_ct_MSForm)
            {
                throw new InvalidOperationException("Component type must be 'vbext_ComponentType.vbext_ct_MSForm'.");
            }

            _component = component;
            _mockProjectBuilder = mockProjectBuilder;
            _vbControls = CreateControlsMock();
        }

        /// <summary>
        /// Adds a <see cref="Control"/> to the form.
        /// </summary>
        /// <param name="name">The name of the control to add.</param>
        /// <returns></returns>
        public MockUserFormBuilder AddControl(string name)
        {
            var control = new Mock<Control>();
            control.SetupProperty(m => m.Name, name);

            _controls.Add(control);
            return this;
        }

        /// <summary>
        /// Builds the UserForm, adds it to the project,
        /// and returns a <see cref="MockProjectBuilder"/>
        /// to continue adding components to the project.
        /// </summary>
        /// <returns></returns>
        public MockProjectBuilder MockProjectBuilder()
        {
            _mockProjectBuilder.AddComponent(Build());
            return _mockProjectBuilder;
        }

        /// <summary>
        /// Gets the mock <see cref="UserForm"/> component.
        /// </summary>
        /// <returns></returns>
        public Mock<VBComponent> Build()
        {
            var designer = CreateMockDesigner();
            _component.SetupGet(m => m.Designer).Returns(() => designer);

            return _component;
        }

        private Mock<UserForm> CreateMockDesigner()
        {
            var result = new Mock<UserForm>();
            result.SetupGet(m => m.Controls).Returns(() => _vbControls.Object);

            return result;
        }

        private Mock<Controls> CreateControlsMock()
        {
            var result = new Mock<Controls>();
            result.Setup(m => m.GetEnumerator()).Returns(() => _controls.GetEnumerator());
            result.As<IEnumerable>().Setup(m => m.GetEnumerator()).Returns(() => _controls.GetEnumerator());

            result.Setup(m => m.Item(It.IsAny<int>())).Returns<int>(index => _controls.ElementAt(index).Object);
            result.SetupGet(m => m.Count).Returns(_controls.Count);
            return result;
        }
    }
}

I love the builder pattern, I find it's definitely the right tool for the job here... except I feel like I've somehow bastardized it, both with the "shortcut" builder method and with the "nested builders" that enable the fluent API, which require the calling test code to "walk back up" to the MockVbeBuilder instance to call Build on the right type, in order to get a Mock<VBE> instance.

On the other hand, using the, well, fluent fluent API or not is in the hands of the caller. Take the example mock setup I gave at the top of this post - it could just as well be built like this:

var vbeBuilder = new MockVbeBuilder();

var project1Builder = vbeBuilder.ProjectBuilder("TestProject1", vbext_ProjectProtection.vbext_pp_none)
    .AddComponent("TestModule1", vbext_ComponentType.vbext_ct_StdModule, contentForProject1Module1)
    .AddComponent("TestModule2", vbext_ComponentType.vbext_ct_StdModule, contentForProject1Module2)
    .AddComponent("TestClass1", vbext_ComponentType.vbext_ct_ClassModule, contentForProject1Class1)
    .AddComponent("ThisWorkbook", vbext_ComponentType.vbext_ct_Document, contentForProject1ThisWorkbook);

var form1 = project1Builder.UserFormBuilder("UserForm1", codeBehindForProject1UserForm1)
    .AddControl("Button1")
    .AddControl("Button2")
    .Build();
project1Builder.AddComponent(form1);

var project1 = project1Builder.Build();
var project2 = vbeBuilder.ProjectBuilder("TestProject2", vbext_ProjectProtection.vbext_pp_locked)
    .AddComponent("TestClass1", vbext_ComponentType.vbext_ct_ClassModule, contentForProject2Class1)
    .AddReference("TestProject1", "PathToProject1")
    .Build();
var mock = vbeBuilder.AddProject(project1)
    .AddProject(project2)
    .Build();

However that feels overly verbose. Is there a better way? Am I really abusing the builder pattern or I've done The Right Thing™?

\$\endgroup\$
6
\$\begingroup\$

It's very hard to follow the builder when it's jumping around like it is... I wonder whether you could achieve something like:

var Mock<VBE> mock = new MockVbeBuilder()
        .AddProject(settings => 
        {
            settings.Name = "";
            settings.Protection = /* something... */;
            settings.ComponentBuilder
                    .AddComponent("TestModule1",
                            vbext_ComponentType.vbext_ct_StdModule, 
                            contentForProject1Module1)
                    .AddComponent("TestModule2", 
                            vbext_ComponentType.vbext_ct_StdModule,
                            contentForProject1Module2);
        })
        .Build();

It's probably going to complicate the design a bit but I think the addition of some Settings type objects may help you in the long run.

I realise that the above isn't really a review of your code... I can whine about some of your naming if you'd like ;)

Naming

You're inconsistent with capitalisation on Vbe/VBE the class is VBE but you CreateVbeMock. I'm guessing that's because VBE is a generated class?

The documentation for MockProjectBuilder (and what it does) is a lot more than just navigating to the project builder.

UserFormBuilder method doesn't quite follow the pattern elsewhere - should be MockUserFormBuilder.

Those are the only ones that I can see... Your code is getting too consistently good to pick out many things!

I wonder whether calling your methods that navigate to a parent builder ToXyzBuilder would make the code read a bit better?

\$\endgroup\$
  • \$\begingroup\$ Is anything off with the naming? \$\endgroup\$ – Mathieu Guindon Jul 15 '15 at 17:15
  • \$\begingroup\$ @Mat'sMug Updated with some grasping at straws naming 'issues'. \$\endgroup\$ – RobH Jul 15 '15 at 19:15
  • \$\begingroup\$ Nice addition, thanks! VBE is a class/interface from the VBIDE API. Its capitalization as part of a member name is utterly annoying indeed, I need to decide once and for all if I stick to the API naming and call that Smurf, VBESmurf, or stick to C# conventions and call it VbeSmurf! Lowercase vbe is only used for locals though, but you raise very good points! \$\endgroup\$ – Mathieu Guindon Jul 15 '15 at 22:57
  • 1
    \$\begingroup\$ @Mat'sMug - I meant Vbe/VBE ;) I'd probably stick to C# convention where you can - it looks much nicer imho \$\endgroup\$ – RobH Jul 16 '15 at 6:20
  • 1
    \$\begingroup\$ Yeah.. unfortunately the COM interface is named in SCREAMCASE as VBE. I have comments disabling warnings all over the project that are to the effect of "Shut up. It's not my naming that sucks, Microsoft named it." \$\endgroup\$ – RubberDuck Jul 16 '15 at 9:27
2
\$\begingroup\$

For my NZD$0.02, I don't like the fluent API as it stands. Its relates to the reasoning behind when, and when not to use the var keyword. When the type is not obvious, be explicit.

In your case, the actual type that each chained method calls on, changes when you call .MockProjectBuilder() and .MockVbeBuilder(). But the actual type is not clear. I reckon that, although more verbose, your second alternative is clearer as it associates the actions performed at different levels together.

You could add specific methods for each type of module so that you don't have to specify the type (the enum is terribly verbose). E.g. .AddStdModule("TestModule1", contentForProject1Module1)

Consider whether you are missing the point of a mock. You are basically implementing the entire API using mocks (a lot of code), which is itself, not tested. Surely, just creating test classes without the overhead of the mocking framework would be cleaner? You could even write unit tests for the test classes (if you were crazy).

\$\endgroup\$

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.