Because of the coupling with the VBIDE API (the extensibility library for the VBA IDE), unit testing the rubberduck 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).
2 Answers 2
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.
-
3\$\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– Mathieu Guindon2015年07月09日 13:48:47 +00:00Commented Jul 9, 2015 at 13:48
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
.
-
22\$\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– Mathieu Guindon2015年07月09日 14:05:18 +00:00Commented Jul 9, 2015 at 14:05
-
2\$\begingroup\$ @Mat'sMug Laziness, bad habits? \$\endgroup\$BadHorsie– BadHorsie2015年07月09日 15:46:39 +00:00Commented Jul 9, 2015 at 15:46
-
3\$\begingroup\$ @BadHorsie What's lazy about writing it the longer way? :D \$\endgroup\$Luaan– Luaan2015年07月09日 20:18:18 +00:00Commented Jul 9, 2015 at 20:18
-
1\$\begingroup\$ Is there a
??=
operator in VBA? :-D \$\endgroup\$John Dvorak– John Dvorak2015年07月10日 04:47:55 +00:00Commented Jul 10, 2015 at 4:47 -
3\$\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– BadHorsie2015年07月10日 10:00:35 +00:00Commented Jul 10, 2015 at 10:00
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 anotherTestInitialize
in a child class. \$\endgroup\$