24
\$\begingroup\$

Because of the coupling with the VBIDE API (the extensibility library for the VBA IDE), unit testing the refactorings, inspections and quick-fixes has been pretty much impossible, at least until a MockFactory was implemented, to do things like this:

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

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

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

The MockFactory is used extensively in an abstract class from which to derive all unit tests that need to work with the VBIDE API:

using System.Collections.Generic;
using System.Linq;
using Microsoft.Vbe.Interop;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Rubberduck.VBEditor;
using MockFactory = RubberduckTests.Mocks.MockFactory;

namespace RubberduckTests
{
    public abstract class VbeTestBase
    {
        private Mock<VBE> _ide;
        private ICollection<VBProject> _projects;

        [TestInitialize]
        public void Initialize()
        {
            _ide = MockFactory.CreateVbeMock();
            _ide.SetupProperty(m => m.ActiveCodePane);
            _ide.SetupProperty(m => m.ActiveVBProject);
            _ide.SetupGet(m => m.SelectedVBComponent).Returns(() => _ide.Object.ActiveCodePane.CodeModule.Parent);
            _ide.SetupGet(m => m.ActiveWindow).Returns(() => _ide.Object.ActiveCodePane.Window);

            _projects = new List<VBProject>();
            var projects = MockFactory.CreateProjectsMock(_projects);
            projects.Setup(m => m.Item(It.IsAny<int>())).Returns<int>(i => _projects.ElementAt(i));

            _ide.SetupGet(m => m.VBProjects).Returns(() => projects.Object);
        }

        [TestCleanup]
        public void Cleanup()
        {
            _ide = null;
        }

        protected QualifiedSelection GetQualifiedSelection(Selection selection)
        {
            if (_ide.Object.ActiveCodePane == null)
            {
                _ide.Object.ActiveVBProject = _ide.Object.VBProjects.Item(0);
                _ide.Object.ActiveCodePane = _ide.Object.ActiveVBProject.VBComponents.Item(0).CodeModule.CodePane;
            }
            return GetQualifiedSelection(selection, _ide.Object.ActiveCodePane.CodeModule.Parent);
        }

        protected QualifiedSelection GetQualifiedSelection(Selection selection, VBComponent component)
        {
            return new QualifiedSelection(new QualifiedModuleName(component), selection);
        }

        protected Mock<VBProject> SetupMockProject(string inputCode, string projectName = null, string moduleName = null, vbext_ComponentType? componentType = null)
        {
            if (componentType == null)
            {
                componentType = vbext_ComponentType.vbext_ct_StdModule;
            }

            if (moduleName == null)
            {
                moduleName = componentType == vbext_ComponentType.vbext_ct_StdModule 
                    ? "Module1" 
                    : componentType == vbext_ComponentType.vbext_ct_ClassModule
                        ? "Class1"
                        : componentType == vbext_ComponentType.vbext_ct_MSForm
                            ? "Form1"
                            : "Document1";
            }

            if (projectName == null)
            {
                projectName = "VBAProject";
            }

            var component = CreateMockComponent(inputCode, moduleName, componentType.Value);
            var components = new List<Mock<VBComponent>> {component};

            var project = CreateMockProject(projectName, vbext_ProjectProtection.vbext_pp_none, components);
            return project;
        }

        protected Mock<VBProject> CreateMockProject(string name, vbext_ProjectProtection protection, ICollection<Mock<VBComponent>> components)
        {
            var project = MockFactory.CreateProjectMock(name, protection);
            var projectComponents = SetupMockComponents(components, project.Object);

            project.SetupGet(m => m.VBE).Returns(_ide.Object);
            project.SetupGet(m => m.VBComponents).Returns(projectComponents.Object);

            _projects.Add(project.Object);
            return project;
        }

        protected Mock<VBComponent> CreateMockComponent(string content, string name, vbext_ComponentType type)
        {
            var module = SetupMockCodeModule(content, name);
            var component = MockFactory.CreateComponentMock(name, module.Object, type, _ide);

            module.SetupGet(m => m.Parent).Returns(component.Object);
            return component;
        }


        private Mock<VBComponents> SetupMockComponents(ICollection<Mock<VBComponent>> items, VBProject project)
        {
            var components = MockFactory.CreateComponentsMock(items, project);
            components.SetupGet(m => m.Parent).Returns(project);
            components.SetupGet(m => m.VBE).Returns(_ide.Object);
            components.Setup(m => m.Item(It.IsAny<int>())).Returns((int index) => items.ElementAt(index).Object);
            components.Setup(m => m.Item(It.IsAny<string>())).Returns((string name) => items.Single(e => e.Name == name).Object);

            return components;
        }

        private Mock<CodeModule> SetupMockCodeModule(string content, string name)
        {
            var codePane = MockFactory.CreateCodePaneMock(_ide, name);
            var module = MockFactory.CreateCodeModuleMock(content, codePane, _ide);

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

I'm pretty confident that this setup will allow us to write a bunch of unit tests for the reference resolver, refactorings and code inspections.

The SetupMockProject overload with the optional parameters was already called by 39 tests when I started refactoring it to support mocking an IDE with as many code modules and projects as needed (I still need to make it support project references and form designer though); in order to keep the existing tests green and the test project compilable, I decided to add optional parameters... and I'm not sure I like the result.

Other than that, I find the code pretty clean and the resulting API pretty neat. Anything I missed? The MockFactory class (as is the rest of the project) is on GitHub, for reference (points to this version of the code).

\$\endgroup\$
  • \$\begingroup\$ You'll also want to make Initialize overridable so child classes can safely add to the test initialization. As it is, I don't think there's a guarantee of execution order of you add another TestInitialize in a child class. \$\endgroup\$ – RubberDuck Jul 9 '15 at 13:31
16
\$\begingroup\$

Almost all of this code can (should?) be moved directly into the MockFactory.

        [TestInitialize]
        public void Initialize()
        {
            _ide = MockFactory.CreateVbeMock();
            _ide.SetupProperty(m => m.ActiveCodePane);
            _ide.SetupProperty(m => m.ActiveVBProject);
            _ide.SetupGet(m => m.SelectedVBComponent).Returns(() => _ide.Object.ActiveCodePane.CodeModule.Parent);
            _ide.SetupGet(m => m.ActiveWindow).Returns(() => _ide.Object.ActiveCodePane.Window);

            _projects = new List<VBProject>();
            var projects = MockFactory.CreateProjectsMock(_projects);
            projects.Setup(m => m.Item(It.IsAny<int>())).Returns<int>(i => _projects.ElementAt(i));

            _ide.SetupGet(m => m.VBProjects).Returns(() => projects.Object);
        }

I imagine that the initialize method could be much simpler and look like this.

_ide = MockFactory.CreateVbeMock();

_projects = new List<VBProject>();
var projects = MockFactory.CreateProjectsMock(_projects);

_ide.SetupGet(m => m.VBProjects).Returns(() => projects.Object);

I find this signature to be a bit strange too.

protected Mock<VBProject> SetupMockProject(string inputCode, string projectName = null, string moduleName = null, vbext_ComponentType? componentType = null)

This is probably fine so long as you're only interested in mocking up a project with a single code module, but you'll quickly find yourself in need of a proper collection of modules. This method should have an overload that takes in an IEnumerable<VBComponent>, or perhaps an AddComponent method would be better.

\$\endgroup\$
  • 2
    \$\begingroup\$ That. Exactly that. Several times I found myself wondering if such or such mock setup was in the MockFactory or in the class I was refactoring. Definitely a responsibility overlap here. Bang on! \$\endgroup\$ – Mathieu Guindon Jul 9 '15 at 13:48
20
\$\begingroup\$
if (moduleName == null)
{
    moduleName = componentType == vbext_ComponentType.vbext_ct_StdModule 
        ? "Module1" 
        : componentType == vbext_ComponentType.vbext_ct_ClassModule
            ? "Class1"
            : componentType == vbext_ComponentType.vbext_ct_MSForm
                ? "Form1"
                : "Document1";
}  

Sorry, that I have to say this, but this is UGLY. Why don't you use a dictionary which is easily expandable if needed and would make the getting of the moduleName more shining?

If you don't want a dictionary, you really should extract it to a separate method where you simply should use a switch instead of this ugly and unreadable ternary construct.


Checking items for null by using an if statement and if it is null assign a default value, so it can be handled nicer using the null coalescing operator ??.

So for example this:

if (componentType == null)
{
    componentType = vbext_ComponentType.vbext_ct_StdModule;
}  

would become this:

componentType = componentType ?? vbext_ComponentType.vbext_ct_StdModule;  

After a second glance at the abstract class VbeTestBase I wonder why you decided to make this class abstract. You don't have neither abstract methods nor properties, hence there isn't any reason why this class should be abstract.

\$\endgroup\$
  • 20
    \$\begingroup\$ You know, the worst part is that I know these things. What I don't know is why do I still write code like that. \$\endgroup\$ – Mathieu Guindon Jul 9 '15 at 14:05
  • 2
    \$\begingroup\$ @Mat'sMug Laziness, bad habits? \$\endgroup\$ – BadHorsie Jul 9 '15 at 15:46
  • 2
    \$\begingroup\$ @BadHorsie What's lazy about writing it the longer way? :D \$\endgroup\$ – Luaan Jul 9 '15 at 20:18
  • 1
    \$\begingroup\$ Is there a ??= operator in VBA? :-D \$\endgroup\$ – John Dvorak Jul 10 '15 at 4:47
  • 2
    \$\begingroup\$ @Luaan Not laziness about writing it the long way, laziness about knowing it's wrong but can't be bothered to do it the proper way! \$\endgroup\$ – BadHorsie Jul 10 '15 at 10:00

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.