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).
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\$