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™?