65.9K
CodeProject 正在变化。 阅读更多。
Home

Mo+ - 利用面向模型开发的力量改进遗留系统 (nopCommerce)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.64/5 (6投票s)

2014 年 9 月 30 日

CPOL

32分钟阅读

viewsIcon

12523

downloadIcon

108

投入一天的时间进行面向模型的工作,极大地提高了 nopCommerce 开源电子商务解决方案的单元测试的质量和数量。

引言

面向模型开发 (MOD) 是一种流程,它允许您在开发过程的任何阶段使用简单、集中的 模型 进行开发。 您可以使用面向模型的模式或模板将模型信息转换为源代码和/或文档。 您无需在开始编码之前创建模型,模型可以在开发过程的后期(很多时候)出现。 MOD 可以轻松应用于以前以非面向模型方法开发的遗留系统。

在本文中,我将演示 MOD 应用于现有遗留系统(流行的开源 nopCommerce 电子商务购物车技术)的强大功能。 我将通过创建模型并以面向模型的方式生成单元测试,来展示 MOD 如何极大地提高其持久化单元测试的质量和数量。

我将使用 Mo+ 来完成这项工作。 Mo+ 是第一个完全支持面向模型开发的技术,它使软件开发人员能够强大地扩展他们已有的工作。 Mo+ 将有效方法(如建模(具有完整的模型访问权)、模板驱动的代码生成和面向对象编程)的优点结合起来,形成了一种新的、强大的面向模型的方法。

背景

Mo+ 面向模型编程语言和面向模型开发的 Mo+ Solution Builder IDE 在这篇 Code Project 文章 中有所介绍。 Mo+ 开源技术可在 moplus.codeplex.com 上获取。 如果您打算跟随示例并自行生成单元测试,请从此处下载并安装 Mo+。 该网站还提供视频教程和其他材料。 Mo+ Solution Builder 还包含丰富的板载帮助。 利用这些资源了解更多关于 Mo+ 的一般用法。

此示例中的 nopCommerce 单元测试代码(如前所述)是使用 C# 和 NUnit 实现的。 整体解决方案和项目使用 Visual Studio 进行管理。

随附的下载包含 Mo+ 解决方案文件、模板以及 nopCommerce 单元测试项目。 要运行随附的单元测试,请在 nopcommerce.codeplex.com 上下载安装程序和源代码,并用随附的 Nop.Data.Tests 项目替换现有的项目。

如何将 MOD 应用于遗留系统?

以下是您可以用来将 MOD 应用于遗留系统的基本公式。

创建模型

您需要一个模型才能有效地将 MOD 应用于您的遗留系统。 您可能已经有一个模型,或者您可能拥有可以轻松转换为模型的信息(例如数据库)。

模型需要采用代码生成技术所需的格式(或转换为该格式)。 Mo+ 提供对从其他源创建模型信息的完整支持。 Mo+ 模型通常是“设计无关”的,本质上保存数据和工作流的信息,而您的设计工作则在代码模板中完成。 例如,如果模型包含 Customer 和 Order 等实体,您可以使用这些相同的实体生成多种内容,例如数据访问组件、服务或测试等。

使用模型生成代码

为了使 MOD 有效,您需要通过创建和/或使用一个或多个面向模型的代码模板来使用您的模型生成所需代码。 以下概述了执行此操作的典型过程:

  1. 建立示例模式 - 您需要根据模型建立一个模式来生成一些代码。 对于您的遗留系统,您的初始模式可能已经存在,您只需要找到它们!
  2. 创建原始文本模板 - 您需要创建一到多个模板来生成您的代码。 您从简单开始,只需将您的示例模式作为原始文本放入模板中。
  3. 添加基本模型信息 - 您从添加高级模型信息到模板开始。 这通常是将原始文本与插入模型数据的语法进行简单的搜索和替换。
  4. 添加更深层次的模型信息 - 添加更深层次的模型信息通常包括一对多关系到较低级别的信息。 要添加此类信息,您需要在模板中添加语句,这些语句会“遍历”该模型数据,然后插入您遇到的每个相关项的模型数据(以及相应的原始文本)。
  5. 分而治之 - 您可能需要正确生成更复杂的功能,或者您可能需要捕获模板中的许多特殊条件,以便为模型中的不同元素生成不同的功能。 采取分而治之的方法。 将模板分成更易于理解、调试和重用的更小部分。 捕获特定业务规则或最佳实践的模板尤其适合创建以供其他模板重用。
  6. 调试、集成、调试 - 您可以通过视觉或其他方式调试您的模板,以初步满意结果。 然后,您将模板集成到您的系统中以生成代码。 然后,您将继续在系统中的生成代码进行调试,解决导致编译和/或运行时问题的模板(或模型)问题。

上述过程基本上与您用来利用模型生成代码的技术无关。 Mo+ 旨在支持上述每个步骤,提供对更深层次模型信息的完整且易于访问的访问,让您能够轻松地分而治之更复杂的问题,真正实现面向模型的开发,并提供完整的调试和集成支持。

选择 nopCommerce 应用 MOD

我希望将面向模型开发应用于一个我不太熟悉的遗留系统,以此作为真实世界的例子。 我选择 nopCommerce 的原因如下:

  • 免费、开源 - 这是处理遗留系统的要求。 免费访问开源代码对于本文的演示至关重要。
  • 流行 - nopCommerce 是一个非常流行且有效的电子商务解决方案,下载量非常大,并且(据我所知)在实际应用中得到了广泛使用。
  • 约定和最佳实践 - nopCommerce 的源代码结构良好且易于理解,有明确的证据表明其遵循了命名和组织约定以及其他最佳实践。 这使得 MOD 更易于应用,因为这些约定和最佳实践可以被编入代码。
  • 模型的来源 - 我们需要构建一个模型来应用 MOD,理想情况下,我们需要能够弄清楚模型应该是什么样子,并且不必做太多建模工作。 nopCommerce 是一个利用 SQL Server 数据库的企业级解决方案,因此该数据库是模型的理想来源。

在 nopcommerce.codeplex.com 上阅读更多关于 nopCommerce 的信息,并下载安装程序和源代码。

 

选择 nopCommerce 单元测试进行改进

我清楚地看到了将 MOD 应用于 nopCommerce 的各个层(例如数据层和服务层)的机会。 然而,我选择将 MOD 应用于持久化单元测试,原因如下:

  • 一个好的起点 - 在将 MOD 应用于遗留系统时,通常最好加强测试套件。 这为您提供了机会来测试和验证模型以及系统本身,可能会发现导致模型发生变化的问题。 拥有更好的测试套件,您就能为其他方面的改进打下坚实的基础!
  • 明显的改进且无影响 - 由于我不是 nopCommerce 团队的成员,也无法了解计划的功能更改,因此我不应该自己进行任何功能更改! 我可以重构数据层和服务层的一些区域,但如果重构后的系统功能相同,本文就无法展示易于衡量的收益。 我认为改进单元测试的质量和数量是一个明确的案例,而且不会影响核心系统。
  • 采用 MOD 的机会 - 我希望通过展示在单元测试级别可以快速进行的改进,nopCommerce 开发团队会考虑应用 MOD(和 Mo+)来管理这一层作为开始。

要查看现有的 nopCommerce 持久化单元测试,请执行以下步骤:

  • nopcommerce.codeplex.com 上下载 nopCommerce 源代码。
  • 在 Visual Studio 中打开下载的解决方案。
  • 在 Tests 文件夹中,打开 Nop.Data.Tests 项目。 持久化单元测试位于此处。 继续运行它们!

工作量和结果

在深入探讨 MOD(使用 Mo+)如何改进 nopCommerce 单元测试的细节之前,这里是总体工作量和结果的摘要。

工作量(成本)

就成本而言,我在此面向模型的工作上花费了大约 10 小时,其中包括花费在以下方面的时间:

  • 了解 nopCommerce 的代码组织、数据库和总体单元测试设置
  • 构建 Mo+ 模型以捕获生成单元测试所需的信息
  • 创建生成单元测试所需的 Mo+ 代码模板
  • 将生成的单元测试与单元测试项目和自定义代码集成
  • 总体调试和模板更新,重复,清洗,重复

我是 Mo+ 专家,在编程速度方面是普通程序员,而且我一开始对 nopCommerce 代码完全不熟悉。 一个熟悉架构和最佳实践的 nopCommerce 团队专家,以良好的或更快的编程速度,并且对 Mo+ 有工作经验,应该能够用更少的时间完成这项工作。 对于不熟悉 Mo+ 的人(即使是 nopCommerce 项目的专家)来说,很可能需要更长的时间。

结果(收益)

好的,我们在这项工作中总体上获得了哪些收益? 以下是单元测试的结果:

  • 自定义代码大大减少 - 以前需要手动管理的 5796 行自定义单元测试代码,现在由 467 行 Mo+ 代码管理(行数包括所有模板中的注释和换行符)。 一个概述实体单元测试框架的主代码模板,生成并维护 92 个单元测试类(文件),无需自定义代码。 仍然可以轻松添加自定义单元测试,并且大约有 5 个执行非常特定任务的自定义测试保留在项目中。
  • 提高单元测试数量 - 通过以下方式将单元测试数量从 130 个增加到 312 个:
    • 自动查找并添加了完全缺失的测试
    • 为每个引用添加了一个测试(例如,带有已引用客户的订单测试)
    • 为每个集合添加了一个测试(例如,带有购物车商品集合的客户测试)
    • 轻松添加了新类别的测试(删除和验证)
  • 提高单元测试质量 - 通过以下方式提高了现有单元测试的 质量
    • 确保所有相关属性(包括引用和集合)都已添加以供测试
    • 确保所有相关属性(包括引用和集合)都在至少一个测试中得到验证
  • 编入最佳实践 - 确保所有单元测试都严格遵循 nopCommerce 单元测试约定和最佳实践。 一些自定义单元测试偏离了标准约定。
  • 增强单元测试的能力 - 为轻松增强 nopCommerce 单元测试提供了基础,可以添加新类别的测试并演进最佳实践。 例如,可以轻松添加具有预期故障(如缺少引用或必需值)的测试。 生成测试值的逻辑可以得到改进,并且测试可以轻松包含测试值(达到或超过)最大长度。
  • 对更改的鲁棒性 - 随着 Mo+ 模型和单元测试模板的到位,随着 nopCommerce 数据库的添加和其他更改,单元测试可以自动更新,从而保持单元测试的数量、质量和预期覆盖率。

现在,nopCommerce 系统和团队已经将面向模型的开发基础应用于单元测试,还可以获得其他好处。 有了模型,好处就可以传播到系统中的其他层,这些层的模板可以产生与上述类似的结果。 有了单元测试模板,好处就可以传播到其他系统(如果 nopCommerce 团队计划开发其他内容),模板(以及其中的最佳实践)可以应用于不同的模型,以实现完全不同的单元测试集。

创建模型

在改进持久化单元测试之前,我们需要创建一个模型。 首要任务是创建 nopCommerce SQL Server 数据库。 我通过下载并运行 nopcommerce.codeplex.com 上的安装程序来做到这一点。 我将此数据库命名为 nopCommerceTest,您可以随意命名(如果您已安装 nopCommerce,请继续浏览数据库结构)。

我们将模型创建为一个名为 NopCommerce 的 Mo+ 解决方案(使用 Mo+,从下载中打开 NopCommerceStep1.xml,如果您想了解更多关于 Mo+ 入门的信息,请参阅背景部分)。 要从 nopCommerce 数据库加载模型信息,我们需要添加一个数据库规范源。 在下面的 Mo+ 树视图中,您可以看到此数据库源,并且表单包含连接到数据库的详细信息(您需要更改这些详细信息以匹配您的数据库)。 我们选择 MDLSqlModel 模板(来自示例包,也包含在随附下载中)作为从 SQL Server 数据库加载信息到模型中的起点(在 Mo+ 中,当打开解决方案文件或在解决方案级别执行 Compile Specification Source Data 命令时,模型会更新)。

这个模板集开箱即用,提供了我们想要的模型的大约 90-95% 的内容,包括实体、关系、基本属性、集合以及与其他实体的引用。 请参阅下面的 Mo+ 树视图和图,其中显示了其中一些信息。

那么,我如何确定这个模型对 nopCommerce 的准确性呢? 我注意到 nopCommerce 的数据模型(和数据库)是使用 Entity Framework Code First 构建的,而映射是验证模型的最佳位置。 请参阅下面显示 Nop.Data 项目中映射类组织方式的树视图,以及 CustomerMap 类的代码。

    public partial class CustomerMap : NopEntityTypeConfiguration<Customer>
    {
        public CustomerMap()
        {
            this.ToTable("Customer");
            this.HasKey(c => c.Id);
            this.Property(u => u.Username).HasMaxLength(1000);
            this.Property(u => u.Email).HasMaxLength(1000);

            this.Ignore(u => u.PasswordFormat);

            this.HasMany(c => c.CustomerRoles)
                .WithMany()
                .Map(m => m.ToTable("Customer_CustomerRole_Mapping"));

            this.HasMany<Address>(c => c.Addresses)
                .WithMany()
                .Map(m => m.ToTable("CustomerAddresses"));
            this.HasOptional<Address>(c => c.BillingAddress);
            this.HasOptional<Address>(c => c.ShippingAddress);
        }
    }

通过比较 nopCommerce 源代码和模型,我注意到以下差异:

  1. Mo+ 模型中的所有实体(如 CustomerOrder)都由一个名为 Domain 的功能组织。 nopCommerce 的数据相关类按其他分组(或功能)进行一致组织,例如 Catalog 和 Customers (参见上面的树视图)。
  2. 少数实体和引用名称不同(例如,模型中的 Addres 与映射中的 Address )。
  3. 模型中的所有集合名称都以“List”结尾(例如,Customer 中的 OrderList),而在映射中,名称采用复数形式(例如,Orders)。

在开始创建面向模型的单元测试之前,我们需要纠正这些差异。 我们有两种选择:

  1. 自定义模型更改 - 我们可以编辑模型以进行所需的更改。 即使源数据库发生更改,Mo+ 也会跟踪这些自定义项。
  2. 自动化模型更改 - 我们可以更改规范模板中的规则,让 Mo+ 自动对模型进行更改。 您可以通过 Mo+ 完全控制如何加载模型信息。

对于功能,我决定手动进行,因为我没有看到一种简单的方法可以通过命名约定或扩展属性从数据库架构中提取这些信息。 我手动创建了每个功能,并将每个实体移到其相应的功能中。

对于实体和引用名称,我也手动重命名了它们,因为它们的数量很少。

但是,我想通过更改 spec 模板来处理集合名称。 目前,一个称为 MDLCollectionName 的关系级别模板处理这些规则(随附下载中的 MDLCollectionNameOrig)。 它看起来像这样(此处无法有效地显示格式化的 Mo+ 代码,因此 Mo+ 代码将显示为普通文本或图像):

<%%:
param baseName
var propertyPrefix = MDLPropertyNamePrefix
if (baseName.StartsWith(propertyPrefix) == false)
{
    <%%=propertyPrefix%%>
}
<%%=baseName%%><%%-List%%>
%%>

复数化名称的修改后的代码如下(随附下载中的 MDLCollectionName 模板):

<%%:
param baseName
var collectionName
if (baseName.ToLower().EndsWith("y") == true)
{
    collectionName = baseName
}
else if (baseName.ToLower().EndsWith("s") == true || baseName.ToLower().EndsWith("x") == true)
{
    collectionName = baseName + "es"
}
else
{
    collectionName = baseName + "s"
}
<%%=collectionName%%>
%%>

重新加载更新后的模板模型后,我发现一些集合名称仍然不正确,于是手动重命名了它们。 下图展示了树视图中的一些模型更改(使用 Mo+,从下载中打开 NopCommerceStep2.xml)。 在树视图中,请注意一些元素是深色文本。 这些元素包含自定义更改。 表单显示了 BlogPost 实体,并说明了功能已更改(“Feature”标签为深色文本)。

此时,通过目视检查,模型对我来说看起来不错,现在是时候继续了。 毫无疑问,我可能在模型中遗漏了一些细节,但随着我生成和集成单元测试,这些细节会(通常以编译错误的形式)显现出来。

创建初始单元测试模板

为遗留系统(通常也是如此)创建面向模型的模板必须从实践中一个好的示例模式开始。 Nop.Data.Tests 项目中的绝大多数持久化测试都遵循保存实体(数据库记录)、获取实体并验证结果的标准约定。 存在用于保存和验证实体本身以及保存带有引用和集合的实体的测试。

建立示例模式

为遗留系统(通常也是如此)创建面向模型的模板必须从实践中一个好的示例模式开始。 Nop.Data.Tests 项目中的绝大多数持久化测试都遵循保存实体(数据库记录)、获取实体并验证结果的标准约定。 存在用于保存和验证实体本身以及保存带有引用和集合的实体的测试。

因此,我选择 BlogPostPersistenceTests 作为开始的良好示例,因为它不是太大,并且包含了大部分我需要的模式。

using System;
using Nop.Core.Domain.Blogs;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Localization;
using Nop.Tests;
using NUnit.Framework;

namespace Nop.Data.Tests.Blogs
{
    [TestFixture]
    public class BlogPostPersistenceTests : PersistenceTest
    {
        [Test]
        public void Can_save_and_load_blogPost()
        {
            var blogPost = new BlogPost
            {
                Title = "Title 1",
                Body = "Body 1",
                AllowComments = true,
                CommentCount = 1,
                Tags = "Tags 1",
                StartDateUtc = new DateTime(2010, 01, 01),
                EndDateUtc = new DateTime(2010, 01, 02),
                CreatedOnUtc = new DateTime(2010, 01, 03),
                MetaTitle = "MetaTitle 1",
                MetaDescription = "MetaDescription 1",
                MetaKeywords = "MetaKeywords 1",
                LimitedToStores = true,
                Language = new Language()
                {
                    Name = "English",
                    LanguageCulture = "en-Us",
                }
            };

            var fromDb = SaveAndLoadEntity(blogPost);
            fromDb.ShouldNotBeNull();
            fromDb.Title.ShouldEqual("Title 1");
            fromDb.Body.ShouldEqual("Body 1");
            fromDb.AllowComments.ShouldEqual(true);
            fromDb.CommentCount.ShouldEqual(1);
            fromDb.Tags.ShouldEqual("Tags 1");
            fromDb.StartDateUtc.ShouldEqual(new DateTime(2010, 01, 01));
            fromDb.EndDateUtc.ShouldEqual(new DateTime(2010, 01, 02));
            fromDb.CreatedOnUtc.ShouldEqual(new DateTime(2010, 01, 03));
            fromDb.MetaTitle.ShouldEqual("MetaTitle 1");
            fromDb.MetaDescription.ShouldEqual("MetaDescription 1");
            fromDb.MetaKeywords.ShouldEqual("MetaKeywords 1");
            fromDb.LimitedToStores.ShouldEqual(true);

            fromDb.Language.ShouldNotBeNull();
            fromDb.Language.Name.ShouldEqual("English");
        }

        [Test]
        public void Can_save_and_load_blogPost_with_blogComments()
        {
            var blogPost = new BlogPost
            {
                Title = "Title 1",
                Body = "Body 1",
                AllowComments = true,
                CreatedOnUtc = new DateTime(2010, 01, 01),
                Language = new Language()
                {
                    Name = "English",
                    LanguageCulture = "en-Us",
                }
            };
            blogPost.BlogComments.Add
                (
                    new BlogComment
                    {
                        CreatedOnUtc = new DateTime(2010, 01, 03),
                        Customer = GetTestCustomer()
                    }
                );
            var fromDb = SaveAndLoadEntity(blogPost);
            fromDb.ShouldNotBeNull();

            fromDb.BlogComments.ShouldNotBeNull();
            (fromDb.BlogComments.Count == 1).ShouldBeTrue();
        }

        protected Customer GetTestCustomer()
        {
            return new Customer
            {
                CustomerGuid = Guid.NewGuid(),
                CreatedOnUtc = new DateTime(2010, 01, 01),
                LastActivityDateUtc = new DateTime(2010, 01, 02)
            };
        }
    }
}

请注意,此类包含一个基本的 Can_save_and_load_blogPost 测试以及一个用于 BlogComments 集合的 Can_save_and_load_with_blogComments 测试。 我绝对想将这两种方法用作模式。

我也知道存在针对实体和相应可空引用的测试,但该示例在此处不存在。 它确实存在于 OrderPersistenceTests 中,例如 Can_save_and_load_order_with_shipping_address ,它测试带有其运送地址引用的订单。

       [Test]
        public void Can_save_and_load_order_with_shipping_address()
        {
            var order = new Order
            {
                OrderGuid = Guid.NewGuid(),
                Customer = GetTestCustomer(),
                BillingAddress = GetTestBillingAddress(),
                ShippingAddress = GetTestShippingAddress(),
                CreatedOnUtc = new DateTime(2010, 01, 04)
            };

            var fromDb = SaveAndLoadEntity(order);
            fromDb.ShouldNotBeNull();
            fromDb.ShippingAddress.ShouldNotBeNull();
            fromDb.ShippingAddress.FirstName.ShouldEqual("FirstName 2");
        }

我将使用这三个测试方法作为面向模型的单元测试的模式。

创建原始文本模板

为了开始从这些最佳实践示例创建模板,我们只需在 Mo+ 中创建一个“原始文本”实体级别代码模板,其中包含 BlogPostPersistenceTests 类和 OrderPersistenceTests 中的额外方法(加上几个相关的 GetTest 方法)。 在下面显示的模板部分中,橙色文本将被生成为字面文本。 此时不插入任何模型数据(请参阅下载中的 NopDataTestStep1 模板)。

在 Mo+ 调试器(F5)中运行此模板会发出以下 C# 代码:

using System;
using System.Linq;
using Nop.Core.Domain.Blogs;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Localization;
using Nop.Tests;
using NUnit.Framework;

namespace Nop.Data.Tests.Blogs
{
    [TestFixture]
    public class BlogPostPersistenceTests : PersistenceTest
    {
        [Test]
        public void Can_save_and_load_blogPost()
        {
            var blogPost = new BlogPost
            {
                Title = "Title 1",
                Body = "Body 1",
                AllowComments = true,
                CommentCount = 1,
                Tags = "Tags 1",
                StartDateUtc = new DateTime(2010, 01, 01),
                EndDateUtc = new DateTime(2010, 01, 02),
                CreatedOnUtc = new DateTime(2010, 01, 03),
                MetaTitle = "MetaTitle 1",
                MetaDescription = "MetaDescription 1",
                MetaKeywords = "MetaKeywords 1",
                LimitedToStores = true,
                Language = new Language()
                {
                    Name = "English",
                    LanguageCulture = "en-Us",
                }
            };

            var fromDb = SaveAndLoadEntity(blogPost);
            fromDb.ShouldNotBeNull();
            fromDb.Title.ShouldEqual("Title 1");
            fromDb.Body.ShouldEqual("Body 1");
            fromDb.AllowComments.ShouldEqual(true);
            fromDb.CommentCount.ShouldEqual(1);
            fromDb.Tags.ShouldEqual("Tags 1");
            fromDb.StartDateUtc.ShouldEqual(new DateTime(2010, 01, 01));
            fromDb.EndDateUtc.ShouldEqual(new DateTime(2010, 01, 02));
            fromDb.CreatedOnUtc.ShouldEqual(new DateTime(2010, 01, 03));
            fromDb.MetaTitle.ShouldEqual("MetaTitle 1");
            fromDb.MetaDescription.ShouldEqual("MetaDescription 1");
            fromDb.MetaKeywords.ShouldEqual("MetaKeywords 1");
            fromDb.LimitedToStores.ShouldEqual(true);

            fromDb.Language.ShouldNotBeNull();
            fromDb.Language.Name.ShouldEqual("English");
        }

        [Test]
        public void Can_save_and_load_blogPost_with_blogComments()
        {
            var blogPost = new BlogPost
            {
                Title = "Title 1",
                Body = "Body 1",
                AllowComments = true,
                CreatedOnUtc = new DateTime(2010, 01, 01),
                Language = new Language()
                {
                    Name = "English",
                    LanguageCulture = "en-Us",
                }
            };
            blogPost.BlogComments.Add
                (
                    new BlogComment
                    {
                        CreatedOnUtc = new DateTime(2010, 01, 03),
                        Customer = GetTestCustomer()
                    }
                );
            var fromDb = SaveAndLoadEntity(blogPost);
            fromDb.ShouldNotBeNull();

            fromDb.BlogComments.ShouldNotBeNull();
            (fromDb.BlogComments.Count == 1).ShouldBeTrue();
        }

        [Test]
        public void Can_save_and_load_order_with_shipping_address()
        {
            var order = new Order
            {
                OrderGuid = Guid.NewGuid(),
                Customer = GetTestCustomer(),
                BillingAddress = GetTestBillingAddress(),
                ShippingAddress = GetTestShippingAddress(),
                CreatedOnUtc = new DateTime(2010, 01, 04)
            };

            var fromDb = SaveAndLoadEntity(order);
            fromDb.ShouldNotBeNull();
            fromDb.ShippingAddress.ShouldNotBeNull();
            fromDb.ShippingAddress.FirstName.ShouldEqual("FirstName 2");
        }

        protected Customer GetTestCustomer()
        {
            return new Customer
            {
                CustomerGuid = Guid.NewGuid(),
                CreatedOnUtc = new DateTime(2010, 01, 01),
                LastActivityDateUtc = new DateTime(2010, 01, 02)
            };
        }

        protected Address GetTestShippingAddress()
        {
            return new Address()
            {
                FirstName = "FirstName 2",
                LastName = "LastName 2",
                Email = "Email 2",
                Company = "Company 2",
                City = "City 2",
                Address1 = "Address2a",
                Address2 = "Address2b",
                ZipPostalCode = "ZipPostalCode 2",
                PhoneNumber = "PhoneNumber 2",
                FaxNumber = "FaxNumber 2",
                CreatedOnUtc = new DateTime(2010, 01, 01),
                Country = GetTestCountry()
            };
        }

        protected Address GetTestBillingAddress()
        {
            return new Address()
            {
                FirstName = "FirstName 1",
                LastName = "LastName 1",
                Email = "Email 1",
                Company = "Company 1",
                City = "City 1",
                Address1 = "Address1a",
                Address2 = "Address1a",
                ZipPostalCode = "ZipPostalCode 1",
                PhoneNumber = "PhoneNumber 1",
                FaxNumber = "FaxNumber 1",
                CreatedOnUtc = new DateTime(2010, 01, 01),
                Country = GetTestCountry()
            };
        }
    }
}

添加基本模型信息(实体和功能)

从这个基本的原始文本模板开始,我们通过一系列步骤来修改模板,将原始文本替换为模型数据,添加确定文本何时以及如何生成的其他逻辑,并在遇到更复杂的内容时将其分解成更易于管理的部分。

从简单开始总是个好主意。 对于这样的实体级别模板,第一步是用实际模型数据替换实体和父级功能信息。 在我们的原始文本中,功能是 Blogs,实体是 BlogPost 和 Order (因为我们从两个不同的测试中获取了内容)。 我们对这些元素进行搜索和替换,以替换为模型数据(FeatureName 和 EntityName)。 在下面说明的模板文本部分中,功能名称插入在第 19 行,实体名称插入在第 22 行和第 25 行,依此类推(参见下载中的 NopDataTestStep2 模板)。 模型元素在模板中显示为青色文本。

我们还需要生成对 Nop.Core.Domain 中实际数据类的引用。 为了保持简单,我们暂时盲目地为每个功能生成一个对相应 Nop.Core.Domain 类的引用(下方第 12-16 行)。 语句显示为蓝色文本。

当您在 Mo+ 中调试模板时,会选择模型中的一个随机实体(在本例中是随机实体)。 您还可以设置断点并键入任意数量的监视表达式来查看正在发生的情况,如下所示。 特殊的 Text 属性允许您在达到断点时查看已生成的文本。

以下是一个生成的持久化测试类的示例(在本例中为 ForumGroup)。

using System;
using System.Linq;
using Nop.Tests;
using NUnit.Framework;
using Nop.Core.Domain.Affiliates;
using Nop.Core.Domain.Blogs;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Configuration;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Directory;
using Nop.Core.Domain.Discounts;
using Nop.Core.Domain.Domain;
using Nop.Core.Domain.Forums;
using Nop.Core.Domain.Localization;
using Nop.Core.Domain.Logging;
using Nop.Core.Domain.Media;
using Nop.Core.Domain.Messages;
using Nop.Core.Domain.News;
using Nop.Core.Domain.Orders;
using Nop.Core.Domain.Polls;
using Nop.Core.Domain.Security;
using Nop.Core.Domain.Seo;
using Nop.Core.Domain.Shipping;
using Nop.Core.Domain.Stores;
using Nop.Core.Domain.Tasks;
using Nop.Core.Domain.Tax;
using Nop.Core.Domain.Topics;
using Nop.Core.Domain.Vendors;

namespace Nop.Data.Tests.Forums
{
    [TestFixture]
    public class ForumGroupPersistenceTests : PersistenceTest
    {
        [Test]
        public void Can_save_and_load_forumGroup()
        {
            var forumGroup = new ForumGroup
            {
                Title = "Title 1",
                Body = "Body 1",
                AllowComments = true,
                CommentCount = 1,
                Tags = "Tags 1",
                StartDateUtc = new DateTime(2010, 01, 01),
                EndDateUtc = new DateTime(2010, 01, 02),
                CreatedOnUtc = new DateTime(2010, 01, 03),
                MetaTitle = "MetaTitle 1",
                MetaDescription = "MetaDescription 1",
                MetaKeywords = "MetaKeywords 1",
                LimitedToStores = true,
                Language = new Language()
                {
                    Name = "English",
                    LanguageCulture = "en-Us",
                }
            };

            var fromDb = SaveAndLoadEntity(forumGroup);
            fromDb.ShouldNotBeNull();
            fromDb.Title.ShouldEqual("Title 1");
            fromDb.Body.ShouldEqual("Body 1");
            fromDb.AllowComments.ShouldEqual(true);
            fromDb.CommentCount.ShouldEqual(1);
            fromDb.Tags.ShouldEqual("Tags 1");
            fromDb.StartDateUtc.ShouldEqual(new DateTime(2010, 01, 01));
            fromDb.EndDateUtc.ShouldEqual(new DateTime(2010, 01, 02));
            fromDb.CreatedOnUtc.ShouldEqual(new DateTime(2010, 01, 03));
            fromDb.MetaTitle.ShouldEqual("MetaTitle 1");
            fromDb.MetaDescription.ShouldEqual("MetaDescription 1");
            fromDb.MetaKeywords.ShouldEqual("MetaKeywords 1");
            fromDb.LimitedToStores.ShouldEqual(true);

            fromDb.Language.ShouldNotBeNull();
            fromDb.Language.Name.ShouldEqual("English");
        }

        [Test]
        public void Can_save_and_load_forumGroup_with_blogComments()
        {
            var forumGroup = new ForumGroup
            {
                Title = "Title 1",
                Body = "Body 1",
                AllowComments = true,
                CreatedOnUtc = new DateTime(2010, 01, 01),
                Language = new Language()
                {
                    Name = "English",
                    LanguageCulture = "en-Us",
                }
            };
            forumGroup.BlogComments.Add
                (
                    new BlogComment
                    {
                        CreatedOnUtc = new DateTime(2010, 01, 03),
                        Customer = GetTestCustomer()
                    }
                );
            var fromDb = SaveAndLoadEntity(forumGroup);
            fromDb.ShouldNotBeNull();

            fromDb.BlogComments.ShouldNotBeNull();
            (fromDb.BlogComments.Count == 1).ShouldBeTrue();
        }

        [Test]
        public void Can_save_and_load_forumGroup_with_shipping_address()
        {
            var forumGroup = new ForumGroup
            {
                OrderGuid = Guid.NewGuid(),
                Customer = GetTestCustomer(),
                BillingAddress = GetTestBillingAddress(),
                ShippingAddress = GetTestShippingAddress(),
                CreatedOnUtc = new DateTime(2010, 01, 04)
            };

            var fromDb = SaveAndLoadEntity(forumGroup);
            fromDb.ShouldNotBeNull();
            fromDb.ShippingAddress.ShouldNotBeNull();
            fromDb.ShippingAddress.FirstName.ShouldEqual("FirstName 2");
        }

        protected Customer GetTestCustomer()
        {
            return new Customer
            {
                CustomerGuid = Guid.NewGuid(),
                CreatedOnUtc = new DateTime(2010, 01, 01),
                LastActivityDateUtc = new DateTime(2010, 01, 02)
            };
        }

        protected Address GetTestShippingAddress()
        {
            return new Address()
            {
                FirstName = "FirstName 2",
                LastName = "LastName 2",
                Email = "Email 2",
                Company = "Company 2",
                City = "City 2",
                Address1 = "Address2a",
                Address2 = "Address2b",
                ZipPostalCode = "ZipPostalCode 2",
                PhoneNumber = "PhoneNumber 2",
                FaxNumber = "FaxNumber 2",
                CreatedOnUtc = new DateTime(2010, 01, 01),
                Country = GetTestCountry()
            };
        }

        protected Address GetTestBillingAddress()
        {
            return new Address()
            {
                FirstName = "FirstName 1",
                LastName = "LastName 1",
                Email = "Email 1",
                Company = "Company 1",
                City = "City 1",
                Address1 = "Address1a",
                Address2 = "Address1a",
                ZipPostalCode = "ZipPostalCode 1",
                PhoneNumber = "PhoneNumber 1",
                FaxNumber = "FaxNumber 1",
                CreatedOnUtc = new DateTime(2010, 01, 01),
                Country = GetTestCountry()
            };
        }
    }
}

添加更深层次的模型信息(属性、引用、集合)

您应该能够看到上面 ForumGroupPersistenceTests 的属性名称和一些方法名称不正确。 我们的模板中仍然有很多字面文本。

首先,让我们添加基本的属性信息,我们迭代以设置和验证属性值(目前仅为测试值放入“some value”(参见下面第 32-36 行和 47-51 行)。

然后,我们迭代我们的集合以生成按集合的保存和加载方法,并添加基本的集合信息。

然后,我们迭代我们的可空引用以生成按引用的保存和加载方法,并添加基本的实体引用信息。

以下显示了我们更新后的模板生成的单元测试示例(BlogPost)(参见下载中的 NopDataTestStep3 模板)。

using System;
using System.Linq;
using Nop.Tests;
using NUnit.Framework;
using Nop.Core.Domain.Affiliates;
using Nop.Core.Domain.Blogs;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Configuration;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Directory;
using Nop.Core.Domain.Discounts;
using Nop.Core.Domain.Domain;
using Nop.Core.Domain.Forums;
using Nop.Core.Domain.Localization;
using Nop.Core.Domain.Logging;
using Nop.Core.Domain.Media;
using Nop.Core.Domain.Messages;
using Nop.Core.Domain.News;
using Nop.Core.Domain.Orders;
using Nop.Core.Domain.Polls;
using Nop.Core.Domain.Security;
using Nop.Core.Domain.Seo;
using Nop.Core.Domain.Shipping;
using Nop.Core.Domain.Stores;
using Nop.Core.Domain.Tasks;
using Nop.Core.Domain.Tax;
using Nop.Core.Domain.Topics;
using Nop.Core.Domain.Vendors;

namespace Nop.Data.Tests.Blogs
{
    [TestFixture]
    public class BlogPostPersistenceTests : PersistenceTest
    {
        [Test]
        public void Can_save_and_load_blogPost()
        {
            var blogPost = new BlogPost
            {
                Id = "some value",
                LanguageId = "some value",
                Title = "some value",
                Body = "some value",
                AllowComments = "some value",
                CommentCount = "some value",
                Tags = "some value",
                StartDateUtc = "some value",
                EndDateUtc = "some value",
                MetaKeywords = "some value",
                MetaDescription = "some value",
                MetaTitle = "some value",
                LimitedToStores = "some value",
                CreatedOnUtc = "some value",
                Language = new Language()
                {
                    Name = "English",
                    LanguageCulture = "en-Us",
                }
            };

            var fromDb = SaveAndLoadEntity(blogPost);
            fromDb.ShouldNotBeNull();
            fromDb.Id.ShouldEqual("some value");
            fromDb.LanguageId.ShouldEqual("some value");
            fromDb.Title.ShouldEqual("some value");
            fromDb.Body.ShouldEqual("some value");
            fromDb.AllowComments.ShouldEqual("some value");
            fromDb.CommentCount.ShouldEqual("some value");
            fromDb.Tags.ShouldEqual("some value");
            fromDb.StartDateUtc.ShouldEqual("some value");
            fromDb.EndDateUtc.ShouldEqual("some value");
            fromDb.MetaKeywords.ShouldEqual("some value");
            fromDb.MetaDescription.ShouldEqual("some value");
            fromDb.MetaTitle.ShouldEqual("some value");
            fromDb.LimitedToStores.ShouldEqual("some value");
            fromDb.CreatedOnUtc.ShouldEqual("some value");

            fromDb.Language.ShouldNotBeNull();
            fromDb.Language.Name.ShouldEqual("English");
        }

        [Test]
        public void Can_save_and_load_blogPost_with_blogComments()
        {
            var blogPost = new BlogPost
            {
                Title = "Title 1",
                Body = "Body 1",
                AllowComments = true,
                CreatedOnUtc = new DateTime(2010, 01, 01),
                Language = new Language()
                {
                    Name = "English",
                    LanguageCulture = "en-Us",
                }
            };
            blogPost.BlogComments.Add
                (
                    new BlogComment
                    {
                        CreatedOnUtc = new DateTime(2010, 01, 03),
                        Customer = GetTestCustomer()
                    }
                );
            var fromDb = SaveAndLoadEntity(blogPost);
            fromDb.ShouldNotBeNull();

            fromDb.BlogComments.ShouldNotBeNull();
            (fromDb.BlogComments.Count == 1).ShouldBeTrue();
        }

        protected Customer GetTestCustomer()
        {
            return new Customer
            {
                CustomerGuid = Guid.NewGuid(),
                CreatedOnUtc = new DateTime(2010, 01, 01),
                LastActivityDateUtc = new DateTime(2010, 01, 02)
            };
        }

        protected Address GetTestShippingAddress()
        {
            return new Address()
            {
                FirstName = "FirstName 2",
                LastName = "LastName 2",
                Email = "Email 2",
                Company = "Company 2",
                City = "City 2",
                Address1 = "Address2a",
                Address2 = "Address2b",
                ZipPostalCode = "ZipPostalCode 2",
                PhoneNumber = "PhoneNumber 2",
                FaxNumber = "FaxNumber 2",
                CreatedOnUtc = new DateTime(2010, 01, 01),
                Country = GetTestCountry()
            };
        }

        protected Address GetTestBillingAddress()
        {
            return new Address()
            {
                FirstName = "FirstName 1",
                LastName = "LastName 1",
                Email = "Email 1",
                Company = "Company 1",
                City = "City 1",
                Address1 = "Address1a",
                Address2 = "Address1a",
                ZipPostalCode = "ZipPostalCode 1",
                PhoneNumber = "PhoneNumber 1",
                FaxNumber = "FaxNumber 1",
                CreatedOnUtc = new DateTime(2010, 01, 01),
                Country = GetTestCountry()
            };
        }
    }
}

分而治之(添加 GetTest 方法及更多)

请注意,在我们的模板和 nopCommerce 持久化单元测试中,通常会内联 GetTest 方法(例如 GetTestCustomer),以获取相应引用的测试数据。 拥有这些内联方法会产生大量重复代码,而且它会使我们的模板生成这些内联代码变得更加复杂,因此我想消除这种重复。

我选择通过向持久化测试添加一个静态 GetTest 方法来处理此问题,以便在一个位置获取该类型的测试实例(例如,CustomerPersistenceTests 将有一个 GetTestCustomer 方法)。 也许我应该将这些方法放在一个单独的辅助类中,但这个更改可以稍后轻松完成。

作为分而治之策略的一部分,我将此测试方法创建为一个单独的实体级别模板(参见下载中的 NopDataTestGetTestMethodStep4 模板)。 以下是设置此方法的模板的详细信息。

由于 Mo+ 模板是真正面向模型的,您可以像使用其他内置模型属性一样使用您创建的任何模板,甚至在表达式中使用。 如果我们想创建包含所有 GetTest 方法的辅助类,我们可以非常轻松地使用此模板来生成这些方法。 这确实为您提供了有效分而治之的能力。

在我们的主模板中,我们只需生成新 GetTest 方法的内容(下方第 27 行),并且我们继续用模型中的更深层信息替换字面文本。

以下是我们更新后的模板生成的单元测试示例(ProductManufacturer)(参见下载中的 NopDataTestStep4 模板)。

using System;
using System.Linq;
using Nop.Tests;
using NUnit.Framework;
using Nop.Core.Domain.Affiliates;
using Nop.Data.Tests.Affiliates;
using Nop.Core.Domain.Blogs;
using Nop.Data.Tests.Blogs;
using Nop.Core.Domain.Catalog;
using Nop.Data.Tests.Catalog;
using Nop.Core.Domain.Common;
using Nop.Data.Tests.Common;
using Nop.Core.Domain.Configuration;
using Nop.Data.Tests.Configuration;
using Nop.Core.Domain.Customers;
using Nop.Data.Tests.Customers;
using Nop.Core.Domain.Directory;
using Nop.Data.Tests.Directory;
using Nop.Core.Domain.Discounts;
using Nop.Data.Tests.Discounts;
using Nop.Core.Domain.Domain;
using Nop.Data.Tests.Domain;
using Nop.Core.Domain.Forums;
using Nop.Data.Tests.Forums;
using Nop.Core.Domain.Localization;
using Nop.Data.Tests.Localization;
using Nop.Core.Domain.Logging;
using Nop.Data.Tests.Logging;
using Nop.Core.Domain.Media;
using Nop.Data.Tests.Media;
using Nop.Core.Domain.Messages;
using Nop.Data.Tests.Messages;
using Nop.Core.Domain.News;
using Nop.Data.Tests.News;
using Nop.Core.Domain.Orders;
using Nop.Data.Tests.Orders;
using Nop.Core.Domain.Polls;
using Nop.Data.Tests.Polls;
using Nop.Core.Domain.Security;
using Nop.Data.Tests.Security;
using Nop.Core.Domain.Seo;
using Nop.Data.Tests.Seo;
using Nop.Core.Domain.Shipping;
using Nop.Data.Tests.Shipping;
using Nop.Core.Domain.Stores;
using Nop.Data.Tests.Stores;
using Nop.Core.Domain.Tasks;
using Nop.Data.Tests.Tasks;
using Nop.Core.Domain.Tax;
using Nop.Data.Tests.Tax;
using Nop.Core.Domain.Topics;
using Nop.Data.Tests.Topics;
using Nop.Core.Domain.Vendors;
using Nop.Data.Tests.Vendors;

namespace Nop.Data.Tests.Catalog
{
    [TestFixture]
    public class ManufacturerPersistenceTests : PersistenceTest
    {
        public static Manufacturer GetTestManufacturer()
        {
            return new Manufacturer
            {
                Id = "some value",
                Name = "some value",
                Description = "some value",
                ManufacturerTemplateId = "some value",
                MetaKeywords = "some value",
                MetaDescription = "some value",
                MetaTitle = "some value",
                PictureId = "some value",
                PageSize = "some value",
                AllowCustomersToSelectPageSize = "some value",
                PageSizeOptions = "some value",
                PriceRanges = "some value",
                SubjectToAcl = "some value",
                LimitedToStores = "some value",
                Published = "some value",
                Deleted = "some value",
                DisplayOrder = "some value",
                CreatedOnUtc = "some value",
                UpdatedOnUtc = "some value",
            };
        }

        [Test]
        public void Can_save_and_load_manufacturer()
        {
            var manufacturer = new Manufacturer
            {
                Id = "some value",
                Name = "some value",
                Description = "some value",
                ManufacturerTemplateId = "some value",
                MetaKeywords = "some value",
                MetaDescription = "some value",
                MetaTitle = "some value",
                PictureId = "some value",
                PageSize = "some value",
                AllowCustomersToSelectPageSize = "some value",
                PageSizeOptions = "some value",
                PriceRanges = "some value",
                SubjectToAcl = "some value",
                LimitedToStores = "some value",
                Published = "some value",
                Deleted = "some value",
                DisplayOrder = "some value",
                CreatedOnUtc = "some value",
                UpdatedOnUtc = "some value",
            };

            var fromDb = SaveAndLoadEntity(manufacturer);
            fromDb.ShouldNotBeNull();
            fromDb.Id.ShouldEqual("some value");
            fromDb.Name.ShouldEqual("some value");
            fromDb.Description.ShouldEqual("some value");
            fromDb.ManufacturerTemplateId.ShouldEqual("some value");
            fromDb.MetaKeywords.ShouldEqual("some value");
            fromDb.MetaDescription.ShouldEqual("some value");
            fromDb.MetaTitle.ShouldEqual("some value");
            fromDb.PictureId.ShouldEqual("some value");
            fromDb.PageSize.ShouldEqual("some value");
            fromDb.AllowCustomersToSelectPageSize.ShouldEqual("some value");
            fromDb.PageSizeOptions.ShouldEqual("some value");
            fromDb.PriceRanges.ShouldEqual("some value");
            fromDb.SubjectToAcl.ShouldEqual("some value");
            fromDb.LimitedToStores.ShouldEqual("some value");
            fromDb.Published.ShouldEqual("some value");
            fromDb.Deleted.ShouldEqual("some value");
            fromDb.DisplayOrder.ShouldEqual("some value");
            fromDb.CreatedOnUtc.ShouldEqual("some value");
            fromDb.UpdatedOnUtc.ShouldEqual("some value");
        }

        [Test]
        public void Can_save_and_load_manufacturer_with_productManufacturers()
        {
            var manufacturer = new Manufacturer
            {
                Id = "some value",
                Name = "some value",
                Description = "some value",
                ManufacturerTemplateId = "some value",
                MetaKeywords = "some value",
                MetaDescription = "some value",
                MetaTitle = "some value",
                PictureId = "some value",
                PageSize = "some value",
                AllowCustomersToSelectPageSize = "some value",
                PageSizeOptions = "some value",
                PriceRanges = "some value",
                SubjectToAcl = "some value",
                LimitedToStores = "some value",
                Published = "some value",
                Deleted = "some value",
                DisplayOrder = "some value",
                CreatedOnUtc = "some value",
                UpdatedOnUtc = "some value",
            };
            manufacturer.ProductManufacturers.Add
                (
                    new ProductManufacturer
                    {
                        Id = "some value",
                        ProductId = "some value",
                        ManufacturerId = "some value",
                        IsFeaturedProduct = "some value",
                        DisplayOrder = "some value",
                        Manufacturer = ManufacturerPersistenceTests.GetTestManufacturer()
                        Product = ProductPersistenceTests.GetTestProduct()
                    }
                );
            var fromDb = SaveAndLoadEntity(manufacturer);
            fromDb.ShouldNotBeNull();

            fromDb.ProductManufacturers.ShouldNotBeNull();
            (fromDb.ProductManufacturers.Count == 1).ShouldBeTrue();
        }
    }
}

我知道这些模板还有其他细节需要解决,但此时我想在单元测试项目中试用它们。 所以,让我们将这些测试集成到我们的项目中,看看会发生什么!

集成面向模型的单元测试

为了集成我们到目前为止的工作,我们如何设置我们的单元测试模板来为我们的项目生成代码? 使用 Mo+,只需定义我们应该为其生成单元测试的实体,以及在哪里/何时更新单元测试文件。

解决方案模板

在 Mo+ 中,一切都从解决方案级别开始。 下面是解决方案级别模板 GenerateProjects 的输出部分。 模板的输出部分指定在哪里、何时以及如何生成您的输出,让您完全控制代码生成,这在您是团队的一员且无法承受每次都重新生成所有内容时尤其重要。

该模板基本上遍历每个项目,并输出解决方案中为每个项目指定的任何内容。 Template 输出属性(洋红色文本)是一个特殊的属性,它执行与项目关联的实际模板。 如果您从未需要在解决方案级别管理任何文件,那么这就是您一直需要的解决方案级别模板(参见下载中的 GenerateProjects)。

<%%:
progress(Solution.EntityCount * 2 * ProjectCount)

// 为每个定义的项目生成代码
foreach (Project)
{
    CurrentProject = Project
    
    // 输出项目内容
    <%%>Template%%>
}
%%>

我们需要让 Mo+ 知道在生成代码时执行此模板。 我们通过在基本解决方案信息中选择此模板来实现。

项目模板

我们需要一个项目级别的模板来确定我们要为其生成单元测试的实体。 在此模板的输出部分,我们选择为每个没有 Ignore 标签的 Entity 输出(生成)我们的单元测试(NopDataTest 单元测试模板的输出)(参见下载中的 NopDataTests 模板)。 进度语句仅用于在生成代码时增加进度条(我们在解决方案级别模板中定义了进度的工作量)。

<%%:
foreach (Entity where Tags.Contains("Ignore") == false)
{
progress
<%%>NopDataTest%%>
progress
}
%%>

要使用此模板,我们需要在模型中定义一个项目。 下面,我们创建了一个名为 Nop Data Tests 的项目,并选择了我们的 NopDataTests 项目级别模板。 就是这样,现在我们的解决方案级别模板将找到此模板来生成我们的单元测试!

在 Nop.Data.Tests 项目中创建单元测试

为了完成我们的集成步骤,我们需要将生成的单元测试添加到 Nop.Data.Tests 项目中。 在处理大型复杂系统时,尤其是在团队环境中,您无法承受每次都重新生成所有面向模型的代码。 Mo+ 是唯一一项让您能够完全控制如何确定生成文档的位置、时间以及方式的技术。

以下是 NopDataTest 模板的输出部分,它确定您的输出:

  • 在哪里 - 变量 filePath 保存有关单元测试文件创建或更新位置的信息。 我们希望单元测试文件位于与现有单元测试相同的项目和功能文件夹中。 我们选择在文件名中添加 _G 作为约定,以便轻松查看生成的文件。
  • 何时 - 何时更新生成的文件非常重要。 为每个单元测试选择的条件是:
    • 如果文件不存在,则生成它,否则如果满足以下条件则更新文件:
      • 文件包含可以重新生成的指示。 开发人员可以选择更改文件顶部的状态注释,以阻止文件被重新生成(例如,当某个文件有太多异常而不希望重新生成它时)。
      • 并且文件包含已更改的核心生成代码。 Mo+ 允许您定义要忽略的区域,因此例如,您可以选择不重新生成文件,如果差异仅仅是因为谁生成了更新或何时生成的。 Mo+ 还允许您定义要保护的区域,因此您可以在生成的代码中插入自定义代码,并且仅选择在受保护区域之外的代码已更改时才重新生成代码(保留您的自定义代码)。 您可以在 GenerateProjects 解决方案模板的内容区域中看到这些配置设置。
  • 如何 - 在这里,我们只是选择在满足条件时创建或更新单元测试文件(使用 update 语句)。 我们可以选择执行其他操作,例如自动将生成的单元测试添加(或删除)到单元测试项目中,或删除不再相关的生成文件。 Mo+ 示例包中的许多模板都会这样做,但我选择将这里的逻辑保持简单,让开发人员手动添加或删除项目中的生成测试。

<%%:
var filePath = Solution.SolutionDirectory + "\\Tests\\" + Project.Namespace + "\\" + Feature.FeatureName + "\\" + EntityName + "PersistenceTests_G.cs"
if (    File(filePath) == null ||
    (    File(filePath).Contains("<Status>Generated</Status>") == true &&
        File(filePath).FilterProtected().FilterIgnored() != Text.FilterProtected().FilterIgnored()
    )
   )
{
    update(filePath)
}
%%>

生成单元测试

设置好上述集成部分后,只需在 Mo+ 中使用解决方案级别的“Update Output Solution”命令生成单元测试文件,并将它们包含在 Visual Studio 的 Nop.Data.Tests 项目中。

调试和最终的面向模型模板

当您在第一轮集成面向模型的代码时,无疑会遇到编译错误,然后是运行时错误,我当然也遇到了。 这些问题通常会迅速指出您需要在模型或模板中进行更正的地方。 我将不详细介绍我所做的所有更改,但以下是我在调试(同时进行一些改进)期间对模板进行的一些主要更改:

  • 模型问题 - 我发现模型中的一些实体和集合没有相应的映射,尽管数据库中存在相应的信息。 因此,我将这些元素标记为“Ignore”并跳过了为它们生成单元测试。 我还必须重命名或添加其他几个集合。
  • 测试值 - 我们从一开始就知道“some value”测试值行不通。 因此,我创建了一个单独的模板,根据数据类型创建合适的测试值(参见随附下载中的 TestValue 属性模板)。
  • 引用 - 我们知道我们只想创建与其他项目区域所需的引用,因此我创建了一个单独的模板来仅输出所需的引用(参见随附下载中的 NopDataTestReferences)。
  • 按测试类型分而治之 - 我发现将每种类型的测试分解为单独的模板更容易,这样我就可以将创建测试方法的逻辑与创建方法内容的逻辑分开(参见下载中的 NopDataTestSaveAndLoadMethod 实体模板、 NopDataTestSaveAndLoadWithCollectionMethod 集合模板以及 NopDataTestSaveAndLoadWithReferenceMethod 实体引用模板)。
  • 按引用添加更多测试 - 在意识到一些保存和加载测试未能完全测试必需引用值的价值后,我添加了其他测试。 因此,我为所有引用(不仅仅是可空的(可选的))生成了按引用的测试(参见下载中的 NopDataTest 实体模板中的逻辑更改)。
  • 删除测试 - 我添加了其他测试以验证删除是否按预期工作(参见随附下载中的 NopDataTestSaveAndDeleteMethod)。

下面是 NopDataTest 模板的内容,它展示了分而治之的方法,利用其他模板来处理测试方法以及添加注释到测试等内容。

<%%:
<%%=NopDataTestReferences%%>

<%%-
///--------------------------------------------------------------------------------
/// <summary>此类用于执行持久化数据单元测试
/// on %%><%%=EntityName%%><%%- data.</summary>
///
/// 此文件是通过代码生成的,不应手动修改。
/// 您可以在单独的局部类文件中添加其他测试。
/// 如果您需要自定义,请将下面的 Status 值更改为“Generated”以外的其他值,
/// 以防止更改被覆盖。
///
/// <CreatedByUserName>%%><%%=USER%%><%%-</CreatedByUserName>
/// <CreatedDate>%%><%%=NOW%%><%%-</CreatedDate>
/// <Status>Generated</Status>
///--------------------------------------------------------------------------------
namespace %%><%%=Project.Namespace%%><%%-.%%><%%=FeatureName%%><%%-
{
    [TestFixture]
    public partial class %%><%%=EntityName%%><%%-PersistenceTests : PersistenceTest
    {%%>
    //
    // 添加 GetTest 静态方法
    //
    <%%=NopDataTestGetTestMethod%%>
     //
     // 添加保存和加载持久化测试
     //
     <%%=NopDataTestSaveAndLoadMethod%%>
     //
     // 创建保存和删除持久化测试
     //
     <%%=NopDataTestSaveAndDeleteMethod%%>
     
        foreach (EntityReference)
        {
            //
            // 创建保存和加载持久化测试,并添加引用的对象
            //
            <%%=NopDataTestSaveAndLoadWithReferenceMethod%%>
        }
       foreach (Collection  where Tags.Contains("Ignore") == false && ReferencedEntity.Tags.Contains("Ignore") == false)
        {
            //
            // 创建保存和加载持久化测试,并添加集合
            //
            <%%=NopDataTestSaveAndLoadWithCollectionMethod%%>
        }
        <%%-
    }
}
%%>
%%>

下面是用于测试删除的 NopDataTestSaveAndDeleteMethod 的内容。

<%%:
     <%%-
     
        [Test]
        public void Can_save_and_delete_%%><%%=EntityName.CamelCase()%%><%%-()
        {
            var %%><%%=TestVarName%%><%%- = new %%><%%=EntityName%%><%%-
            {%%>
                   foreach (Property where Identity == false && IsForeignKeyMember == false)
                   {
                       log("TestValues", FullName, TestValue)
                    <%%-
                %%><%%=PropertyName%%><%%- = %%><%%=LogValue("TestValues", FullName)%%><%%-,%%>
                  }
                foreach (EntityReference where IsNullable == false)
                {
                <%%-
                %%><%%=EntityReferenceName%%><%%- = %%><%%=ReferencedEntity.EntityName%%><%%-PersistenceTests.GetTest%%><%%=ReferencedEntity.EntityName%%><%%-(),%%>
                }<%%-
           };

            var fromDb = SaveAndLoadEntity(%%><%%=TestVarName%%><%%-, false);
            fromDb.ShouldNotBeNull();%%>
            foreach (Property where Identity == false && IsForeignKeyMember == false && DataTypeCode != 26 /* or save generated guid to test */)
            {
            <%%-
            fromDb.%%><%%=PropertyName%%><%%-.ShouldEqual(%%><%%=LogValue("TestValues", FullName)%%><%%-);%%>
            }
            <%%-
            
            fromDb = DeleteAndLoadEntity( %%><%%=TestVarName%%><%%- );
            fromDb.ShouldBeNull();
        }%%>
%%>

下面是一个最终生成的单元测试文件示例(BlogPostPersistenceTests)。

using System;
using System.Linq;
using Nop.Tests;
using NUnit.Framework;
using Nop.Core.Domain.Blogs;
using Nop.Data.Tests.Blogs;
using Nop.Data.Tests.Customers;
using Nop.Core.Domain.Localization;
using Nop.Data.Tests.Localization;
///--------------------------------------------------------------------------------
/// <summary>This class is used to perform persistence data unit tests
/// on BlogPost data.</summary>
///
/// This file is code generated and should not be modified by hand.
/// You can add additional tests in a separate partial class file.
/// If you need to customize, change the Status value below to something
/// other than Generated to prevent changes from being overwritten.
///
/// <CreatedByUserName>INCODE-1\Dave</CreatedByUserName>
/// <CreatedDate>9/24/2014</CreatedDate>
/// <Status>Generated</Status>
///--------------------------------------------------------------------------------
namespace Nop.Data.Tests.Blogs
{
    [TestFixture]
    public partial class BlogPostPersistenceTests : PersistenceTest
    {
        public static BlogPost GetTestBlogPost()
        {
            return new BlogPost
            {
                Title = "Title 13",
                Body = "Body 14",
                AllowComments = false,
                CommentCount = 15,
                Tags = "Tags 16",
                StartDateUtc = new DateTime(2010, 01, 3),
                EndDateUtc = new DateTime(2010, 01, 4),
                MetaKeywords = "MetaKeywords 17",
                MetaDescription = "MetaDescription 18",
                MetaTitle = "MetaTitle 19",
                LimitedToStores = true,
                CreatedOnUtc = new DateTime(2010, 01, 5),
                Language = LanguagePersistenceTests.GetTestLanguage(),
            };
        }
     
        [Test]
        public void Can_save_and_load_blogPost()
        {
            var blogPost = new BlogPost
            {
                Title = "Title 13",
                Body = "Body 14",
                AllowComments = false,
                CommentCount = 15,
                Tags = "Tags 16",
                StartDateUtc = new DateTime(2010, 01, 3),
                EndDateUtc = new DateTime(2010, 01, 4),
                MetaKeywords = "MetaKeywords 17",
                MetaDescription = "MetaDescription 18",
                MetaTitle = "MetaTitle 19",
                LimitedToStores = true,
                CreatedOnUtc = new DateTime(2010, 01, 5),
                Language = LanguagePersistenceTests.GetTestLanguage(),
           };

            var fromDb = SaveAndLoadEntity(blogPost);
            fromDb.ShouldNotBeNull();
            fromDb.Title.ShouldEqual("Title 13");
            fromDb.Body.ShouldEqual("Body 14");
            fromDb.AllowComments.ShouldEqual(false);
            fromDb.CommentCount.ShouldEqual(15);
            fromDb.Tags.ShouldEqual("Tags 16");
            fromDb.StartDateUtc.ShouldEqual(new DateTime(2010, 01, 3));
            fromDb.EndDateUtc.ShouldEqual(new DateTime(2010, 01, 4));
            fromDb.MetaKeywords.ShouldEqual("MetaKeywords 17");
            fromDb.MetaDescription.ShouldEqual("MetaDescription 18");
            fromDb.MetaTitle.ShouldEqual("MetaTitle 19");
            fromDb.LimitedToStores.ShouldEqual(true);
            fromDb.CreatedOnUtc.ShouldEqual(new DateTime(2010, 01, 5));

            fromDb.Language.ShouldNotBeNull();
        }
     
        [Test]
        public void Can_save_and_delete_blogPost()
        {
            var blogPost = new BlogPost
            {
                Title = "Title 13",
                Body = "Body 14",
                AllowComments = false,
                CommentCount = 15,
                Tags = "Tags 16",
                StartDateUtc = new DateTime(2010, 01, 3),
                EndDateUtc = new DateTime(2010, 01, 4),
                MetaKeywords = "MetaKeywords 17",
                MetaDescription = "MetaDescription 18",
                MetaTitle = "MetaTitle 19",
                LimitedToStores = true,
                CreatedOnUtc = new DateTime(2010, 01, 5),
                Language = LanguagePersistenceTests.GetTestLanguage(),
           };

            var fromDb = SaveAndLoadEntity(blogPost, false);
            fromDb.ShouldNotBeNull();
            fromDb.Title.ShouldEqual("Title 13");
            fromDb.Body.ShouldEqual("Body 14");
            fromDb.AllowComments.ShouldEqual(false);
            fromDb.CommentCount.ShouldEqual(15);
            fromDb.Tags.ShouldEqual("Tags 16");
            fromDb.StartDateUtc.ShouldEqual(new DateTime(2010, 01, 3));
            fromDb.EndDateUtc.ShouldEqual(new DateTime(2010, 01, 4));
            fromDb.MetaKeywords.ShouldEqual("MetaKeywords 17");
            fromDb.MetaDescription.ShouldEqual("MetaDescription 18");
            fromDb.MetaTitle.ShouldEqual("MetaTitle 19");
            fromDb.LimitedToStores.ShouldEqual(true);
            fromDb.CreatedOnUtc.ShouldEqual(new DateTime(2010, 01, 5));
            
            fromDb = DeleteAndLoadEntity( blogPost );
            fromDb.ShouldBeNull();
        }

        [Test]
        public void Can_save_and_load_blogPost_with_Language()
        {
            var blogPost = new BlogPost
            {
                Title = "Title 13",
                Body = "Body 14",
                AllowComments = false,
                CommentCount = 15,
                Tags = "Tags 16",
                StartDateUtc = new DateTime(2010, 01, 3),
                EndDateUtc = new DateTime(2010, 01, 4),
                MetaKeywords = "MetaKeywords 17",
                MetaDescription = "MetaDescription 18",
                MetaTitle = "MetaTitle 19",
                LimitedToStores = true,
                CreatedOnUtc = new DateTime(2010, 01, 5),
                Language = new Language
                {
                    Name = "Name 30",
                    LanguageCulture = "LanguageCulture 31",
                    UniqueSeoCode = "32",
                    FlagImageFileName = "FlagImageFileName 33",
                    Rtl = true,
                    LimitedToStores = false,
                    DefaultCurrencyId = 34,
                    Published = true,
                    DisplayOrder = 35,
                },
            };

            var fromDb = SaveAndLoadEntity(blogPost);
            fromDb.ShouldNotBeNull();
            fromDb.Language.ShouldNotBeNull();

            fromDb.Title.ShouldEqual("Title 13");
            fromDb.Body.ShouldEqual("Body 14");
            fromDb.AllowComments.ShouldEqual(false);
            fromDb.CommentCount.ShouldEqual(15);
            fromDb.Tags.ShouldEqual("Tags 16");
            fromDb.StartDateUtc.ShouldEqual(new DateTime(2010, 01, 3));
            fromDb.EndDateUtc.ShouldEqual(new DateTime(2010, 01, 4));
            fromDb.MetaKeywords.ShouldEqual("MetaKeywords 17");
            fromDb.MetaDescription.ShouldEqual("MetaDescription 18");
            fromDb.MetaTitle.ShouldEqual("MetaTitle 19");
            fromDb.LimitedToStores.ShouldEqual(true);
            fromDb.CreatedOnUtc.ShouldEqual(new DateTime(2010, 01, 5));
            fromDb.Language.Name.ShouldEqual("Name 30");
            fromDb.Language.LanguageCulture.ShouldEqual("LanguageCulture 31");
            fromDb.Language.UniqueSeoCode.ShouldEqual("32");
            fromDb.Language.FlagImageFileName.ShouldEqual("FlagImageFileName 33");
            fromDb.Language.Rtl.ShouldEqual(true);
            fromDb.Language.LimitedToStores.ShouldEqual(false);
            fromDb.Language.DefaultCurrencyId.ShouldEqual(34);
            fromDb.Language.Published.ShouldEqual(true);
            fromDb.Language.DisplayOrder.ShouldEqual(35);
        }
 
        [Test]
        public void Can_save_and_load_blogPost_with_blogComments()
        {
            var blogPost = new BlogPost
            {
                Title = "Title 13",
                Body = "Body 14",
                AllowComments = false,
                CommentCount = 15,
                Tags = "Tags 16",
                StartDateUtc = new DateTime(2010, 01, 3),
                EndDateUtc = new DateTime(2010, 01, 4),
                MetaKeywords = "MetaKeywords 17",
                MetaDescription = "MetaDescription 18",
                MetaTitle = "MetaTitle 19",
                LimitedToStores = true,
                CreatedOnUtc = new DateTime(2010, 01, 5),
                Language = LanguagePersistenceTests.GetTestLanguage(),
             };
             blogPost.BlogComments.Add
                 (
                     new BlogComment
                     {
                         CommentText = "CommentText 12",
                         CreatedOnUtc = new DateTime(2010, 01, 2),
                         Customer = CustomerPersistenceTests.GetTestCustomer(),
                     }
                 );
             var fromDb = SaveAndLoadEntity(blogPost);
             fromDb.ShouldNotBeNull();
             fromDb.Title.ShouldEqual("Title 13");
             fromDb.Body.ShouldEqual("Body 14");
             fromDb.AllowComments.ShouldEqual(false);
             fromDb.CommentCount.ShouldEqual(15);
             fromDb.Tags.ShouldEqual("Tags 16");
             fromDb.StartDateUtc.ShouldEqual(new DateTime(2010, 01, 3));
             fromDb.EndDateUtc.ShouldEqual(new DateTime(2010, 01, 4));
             fromDb.MetaKeywords.ShouldEqual("MetaKeywords 17");
             fromDb.MetaDescription.ShouldEqual("MetaDescription 18");
             fromDb.MetaTitle.ShouldEqual("MetaTitle 19");
             fromDb.LimitedToStores.ShouldEqual(true);
             fromDb.CreatedOnUtc.ShouldEqual(new DateTime(2010, 01, 5));
             
             fromDb.BlogComments.ShouldNotBeNull();
             (fromDb.BlogComments.Count == 1).ShouldBeTrue();
             fromDb.BlogComments.First().CommentText.ShouldEqual("CommentText 12");
             fromDb.BlogComments.First().CreatedOnUtc.ShouldEqual(new DateTime(2010, 01, 2));
        }
    }
}

请查看随附下载中的所有 Mo+ 代码模板,以了解更多详细信息,并查阅随附下载中的所有 Nop.Data.Tests 生成的单元测试。

继续 nopCommerce

我计划联系 nopCommerce 开发团队,并向他们展示本文概述的结果。 我希望开发团队能选择采用 Mo+,至少用于单元测试,并且我能作为贡献者加入他们的项目,并在其他领域提供帮助。 我将根据 nopCommerce 团队的反馈更新本文。

何时应将 MOD 应用于您的遗留系统?

MOD 提供了灵活性,您可以选择应用任意数量的面向模型代码来管理您的遗留系统。 您需要权衡学习 MOD 技术和进行面向模型工作的成本与直接和后续的收益。 以下是一些在遗留系统上应用 MOD 的考虑因素:

  • 您是否有模型,或者值得创建一个模型吗? - 首先,您必须权衡提供 MOD 模型与收益之间的成本。 您是否有任何形式的模型来表示系统的结构和行为? 如果没有,您是否认为拥有这样一个模型对沟通、规划和其他目的有普遍价值? 如果这两个问题的答案都是否定的,并且在确定模型是什么方面付出了很多努力,那么 MOD 可能不适合您的遗留系统。 您可以选择从头开始创建模型,使用 Mo+ 或其他技术。 如果您的系统具有 SQL Server 或 MySQL 等数据库,或者您拥有可以输出为 XML 的 UML 或其他模型,那么您的建模工作对于下游开发来说可能会很轻松。 Mo+ 是独一无二的,因为它具有一个规范模板功能,专门用于减少您已经拥有源模型时的建模工作。 您对如何为开发目的翻译源模型拥有完全的控制权(上面演示了将集合命名为 nopCommerce 实践的一个简单示例)。
  • 您能否在系统中设想面向模型的模式? - 这可能是一个更难回答的问题,但在应用 MOD 之前,您应该设想系统中的某些面向模型的模式,并看到潜在的收益。 例如,假设您的模型包含 Customer、Product 和 Order,并且您有客户、产品和订单的 UI 屏幕。 您是否在这些屏幕上看到数据呈现方式的相似之处(客户姓名在客户屏幕上的显示方式 vs. 产品名称在产品屏幕上的显示方式),或者在执行类似操作的方式(添加客户或添加订单与数据不同但相似)? 如果答案是肯定的,那么您已经设想了一个面向模型的模式! 团队最佳实践对于建立良好的面向模型模式很重要。 可以编入代码的良好最佳实践将使 MOD 的应用更加容易。
  • 您需要哪些即时改进? - 在使用 MOD(或一般情况下)更改遗留系统之前,当然您心中有改进的想法。 您是否计划进行重大的功能更改? 您是否需要填补现有功能的空白或使其更健壮? 您的源代码中是否存在需要纠正的一般不一致之处? 如果任何这些问题的答案是肯定的,那么现在是考虑 MOD 的好时机。
  • 您应该用面向模型的代码管理多少自定义代码? - 这在很大程度上取决于您的系统,但您必须权衡创建和维护面向模型代码(模板)的成本与节省自定义编码工作的收益。 构建一个非常复杂的模板来维护一个类文件可能不值得。 构建一个相当复杂的模板来维护 100 个类文件,允许您为那 10 个非常复杂的类插入自定义代码,这非常值得。 作为初步的经验法则,我认为如果一个新模板可以管理您自定义代码中的 10 个内容,那么就去做吧! 如果您可以重用下载的或您自己的稳定模板,那么就去做吧,即使它只管理您自定义代码中的 1 个内容! Mo+ 在其真正面向模型的代码生成方法方面是独一无二的,它允许您分而治之复杂性,以便您能够以面向模型的方式管理更多的代码,并无缝集成面向模型的代码和自定义代码(上面演示了一些示例)。 这里的另一个观点是,您的目标不一定是为了拥有尽可能多的面向模型的(生成)代码。 如果您在工作中发现可以用健壮、简洁、与模型无关的自定义代码替换面向模型的代码,那么就去做吧!
  • 您是否计划将代码重构为新技术或最佳实践? - 如果您需要对遗留代码进行大量更改以迁移到新语言或技术,或者将大量代码演进为符合您更新后的最佳实践,那么这是一个应用 MOD 的绝佳机会! 您可以比其他自定义代码更快地基于面向模型的模式调试和重构代码。 而且,当您的技术和最佳实践进一步发展时,您的面向模型代码在未来将更容易更改。

总结

希望本文能为您在考虑面向模型开发来更改和增强遗留系统(尤其是像 nopCommerce 这样拥有数据库的企业系统)时提供一些思考。

此外,我希望您尝试 Mo+ 和 Mo+ Solution Builder ,以充分利用将面向模型的方法融入您的开发工作。 免费的开源产品可在 moplus.codeplex.com 上获取。 除了产品本身,该网站还包含用于构建更完整模型和生成完整工作应用程序的示例包。 该网站还提供视频教程和其他材料。 Mo+ Solution Builder 还包含丰富的板载帮助。

成为会员!

Mo+ 社区可以通过网站 https://modelorientedplus.com 获得额外的支持,并为 Mo+ 的发展做出贡献。 成为会员可为 Mo+ 用户提供额外的好处,例如额外的论坛支持、会员贡献的工具以及投票和/或为 Mo+ 的发展方向做出贡献的能力。 此外,我们正在为会员举办竞赛,您可以通过使用 Mo+ 开发解决方案来赢取奖金。

如果您对高效的面向模型软件开发有丝毫兴趣,请注册成为会员。 它是免费的,您不会收到垃圾邮件!
© . All rights reserved.