由于与VBIDE API(VBA IDE的可扩展性库)相结合,因此几乎无法进行单元测试Rubberduck重构,检查和快速修复,至少在实施MockFactory之前,无法执行以下操作: br />
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;
}


MockFactory在抽象类中得到了广泛使用,可以从中抽象出需要使用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;
        }
    }
}


我非常有信心,此设置将使我们能够为参考解析器,重构和代码检查编写大量的单元测试。

带有可选的SetupMockProject重载当我开始对其进行重构以支持使用所需的代码模块和项目来模拟IDE时,已经由39个测试调用了参数(尽管我仍然需要使其支持项目引用和表单设计器);为了使现有测试保持绿色且测试项目可编译,我决定添加可选参数...但不确定我是否喜欢此结果。

除此之外,我找到了代码非常干净,并且生成的API非常简洁。我错过了什么吗? MockFactory类(与项目的其余部分一样)位于GitHub上,以供参考(指向此版本的代码)。

评论

您还需要使Initialize可重写,以便子类可以安全地添加到测试初始化​​中。就这样,我认为不能保证您在子类中添加另一个TestInitialize的执行顺序。

#1 楼

几乎所有这些代码都可以(应该?)直接移到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);
        }



我想初始化方法可能会更简单,看起来像这样。

_ide = MockFactory.CreateVbeMock();

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

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


我也发现此签名有些奇怪。


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



只要您只想用一个代码模块来模拟项目,这可能很好很快就会发现自己需要适当的模块集合。此方法应该具有一个IEnumerable<VBComponent>的重载,或者可能是一个更好的AddComponent方法。

评论


\ $ \ begingroup \ $
那。就是这样几次我发现自己想知道这样的模拟设置是在MockFactory还是在重构的类中。这里肯定有责任重叠。 !
\ $ \ endgroup \ $
–马修·金登(Mathieu Guindon)♦
2015年7月9日在13:48

#2 楼


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



很抱歉,我必须这么说,但这很丑。为什么不使用需要时可以轻松扩展的字典,从而使moduleName的使用更加引人入胜?

如果您不想要字典,那么您真的应该将其提取到一个单独的方法中,只需使用switch而不是这个难看且难以理解的三元结构。


通过使用null语句检查if的项目,如果它是null,则分配默认值,因此可以使用空合并运算符??更好地处理它。

因此,例如:


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



将变成这样:

componentType = componentType ?? vbext_ComponentType.vbext_ct_StdModule;  



再看一眼abstract class VbeTestBase后,我想知道为什么您决定上此类abstract。您既没有abstract方法,也没有属性,因此没有任何理由使此类为abstract

评论


\ $ \ begingroup \ $
你知道,最糟糕的是我知道这些东西。我不知道的是为什么我仍要编写这样的代码。
\ $ \ endgroup \ $
–马修·金登(Mathieu Guindon)♦
2015年7月9日14:05

\ $ \ begingroup \ $
@ Mat'sMug懒惰,不良习惯?
\ $ \ endgroup \ $
– BadHorsie
2015年7月9日在15:46

\ $ \ begingroup \ $
@BadHorsie从更长远的角度写它是什么懒惰? :D
\ $ \ endgroup \ $
–罗安
15年7月9日在20:18

\ $ \ begingroup \ $
VBA中是否有?? =运算符? :-D
\ $ \ endgroup \ $
– John Dvorak
15年7月10日在4:47

\ $ \ begingroup \ $
@Luaan长期编写它并不懒惰,知道它是错误的也很懒惰,但是不能以正确的方式来烦恼!
\ $ \ endgroup \ $
– BadHorsie
2015年7月10日10:00