Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Mo+ - Using the Power of Model Oriented Development to Improve a Legacy System (nopCommerce)

4.64/5 (6 votes)
30 Sep 2014CPOL32 min read 12.2K   83  
Applying a day's worth of model oriented work to greatly improve the quality and quantity of unit tests for the nopCommerce open source e-commerce solution.

Introduction

Model Oriented Development (MOD) is a process that allows you to utilize a simple, focused model for development purposes at any point in the development process.  You translate model information into source code and/or documents using model oriented patterns or templates.  You don't need to create the model before you start coding, the model may come into being (much) later on in the development process.  MOD can easily be applied to a legacy system that was previously developed with a non model oriented approach.

In this article, I will demonstrate the power of MOD as applied to an existing legacy system, the popular open source nopCommerce e-commerce shopping cart technology.  I will demonstrate that MOD can greatly improve the quality and quantity of their persistence unit tests by creating a model and generating these unit tests in a model oriented way.

I will use Mo+ to do this work.  Mo+ is the first technology that fully supports model oriented development, allowing software developers to powerfully scale the work they already do.  Mo+ combines the benefits of effective approaches such as modeling (with complete model access), template driven code generation, and object oriented programming into a new and powerful model oriented approach.

Background

The Mo+ model oriented programming language and the Mo+ Solution Builder IDE for model oriented development was introduced in this Code Project article.  The Mo+ open source technology is available at moplus.codeplex.com.  Download and install Mo+ from here if you intend to follow along with the examples and generate the unit tests on your own.  Video tutorials and other materials are also available at this site. The Mo+ Solution Builder also contains extensive on board help.  Utilize these resources to learn more about how to use Mo+ in general.

The nopCommerce unit test code in this example are (as before) implemented using C# and NUnit.  The overall solution and projects are managed with Visual Studio.

The attached download contains Mo+ solution files, templates, and the nopCommerce unit test project.  To run the attached unit tests, download the installs and source code at nopcommerce.codeplex.com, and replace the Nop.Data.Tests project with the attached one.

How Do You Apply MOD to a Legacy System?

Following is a basic formula you can use to apply MOD to your legacy system.

Creating a Model

You need a model in order to effectively apply MOD to your legacy system.  You may already have one, or you may have information (such as a database) that can be easily translated into a model.

The model needs to be in (or translated to) a format as needed by the technology you choose for code generation.  Mo+ gives you full support for creating model information from other sources.  Mo+ models are generally "design independent" and essentially hold information on data and workflow, and you do your design work in the code templates.  For example, if the model contains entities such as Customer and Order, you can use these same entities to generate multiple things such as data access components, services, or tests, etc.

Using the Model to Generate Code

In order for MOD to be effective, you use your model to generate the code you want by creating and/or using one or more model oriented code templates.  A typical process of doing this is outlined as follows:

  1. Establish an Example Pattern - You need to establish a pattern based on the model in order to generate some code.  With your legacy system, your initial patterns are probably already there, you just need to find them!
  2. Create Raw Text Template(s) - You need to create one or more templates to generate your code.  You start simple by just putting your example patterns as raw text into your templates.
  3. Add Basic Model Information - You start simple by adding high level model information into your template.  This is usually a simple search and replace of raw text with syntax to insert your model data.
  4. Add Deeper Model Information - Adding deeper model information often includes one-to-many relationships to lower level information.  To add this kind of information, you need to add statements into your template that "walks" through this model data and then insert your model data (along with corresponding raw text) for each relevant item encountered.
  5. Divide and Conquer - You may have more complex functionality you need to generate correctly, or you may have a number of special conditions you need to capture in your template to generate functionality differently for various elements in your model.  Take a divide and conquer approach.  Divide your templates into smaller ones that can be more easily understood, debugged, and reused.  Templates that capture specific business rules or best practices are especially good to create for reuse by other templates.
  6. Debug, Integrate, Debug - You debug your template(s) visually or through other means to be initially satisfied with the results.  Then you integrate your template(s) to generate code for your system.  Then you continue to debug with generated code in your system, resolving template (or model) issues that cause compilation and/or run time problems.

The process outlined above can essentially be the same regardless of the technology you use to utilize a model to generate code.  Mo+ is designed to support each of the steps outlined above, providing full and easy access to deeper model information, allowing you to easily divide and conquer more complex problems with a truly model oriented approach, and full debugging and integration support.

Choosing nopCommerce for Applying MOD

I wanted to apply model oriented development to a legacy system that I was unfamiliar with as a real world example.  I chose nopCommerce for the following reasons:

  • Free, Open Source - This was a requirement for a legacy system to work on.  Free access to open source code is essential for demonstration in this article.
  • Popular - nopCommerce is a very popular and effective e-commerce solution with a very high number of downloads and (as far as I can tell) a high level of usage in real applications.
  • Conventions and Best Practices - The nopCommerce source code is well structured and easy to understand, with clear evidence that naming and organizational conventions and other best practices are followed.  This makes it easier to apply MOD, as these conventions and best practices can be codified.
  • Source for a Model - We need to build a model in order to apply MOD, and ideally we need to be able to figure out what the model should be and also not have to do much modeling.  nopCommerce is an enterprise solution utilizing a SQL Server database, so this database is an ideal source for a model.

Read more about nopCommerce, and download the installs and source code at nopcommerce.codeplex.com.

Image 1

 

Choosing nopCommerce Unit Tests for Improvement

I clearly see opportunities for applying MOD to various nopCommerce layers, such as the data and service layers.  However, I chose to apply MOD to the persistence unit tests for the following reasons:

  • Good place to start - When applying MOD to a legacy system, it is usually a good thing to look at beefing up the test suite.  This gives you a chance to exercise and validate the model as well as the system itself, possibly identifying issues that may result in changes to the model.  With a better test suite, you are in a great position to work on improvements elsewhere!
  • Clear improvements without impact - Since I am not a member of the nopCommerce team and have no visibility on planned feature changes, I shouldn't be making any feature changes myself!  I could have refactored some areas in the data and service layers, but if the refactored system is functionally the same, there is no easily measured benefit that can be shown in this article.  I see a clear case for improving the quality and quantity of the unit tests that does not impact the core system.
  • Chance to adopt MOD - I am hoping that by demonstrating the improvements that can be quickly made at the unit test level, that the nopCommerce development team will consider applying MOD (and Mo+) to manage this layer for starters.

To see the existing nopCommerce persistence unit tests, perform the following steps:

  • Download the nopCommerce source code at nopcommerce.codeplex.com.
  • Open up the downloaded solution in Visual Studio.
  • In the Tests folder, open up the Nop.Data.Tests project.  The persistence unit tests are located here.  Go ahead and run them!

The Level of Effort and the Results

Before we dig into the details of how MOD (using Mo+) improved the nopCommerce unit tests, here is a summary of the overall effort and the results.

The Effort (Cost)

In terms of cost, I spent about 10 hours on this model oriented effort, which included time spent on:

  • Getting a handle on the nopCommerce code organization, database, and overall unit test setup
  • Building a Mo+ model to capture information needed to generate the unit tests
  • Creating Mo+ code templates needed to generate the unit tests
  • Integrating the generated unit tests with the unit test project and custom code
  • Overall debugging and template updates, rinse, wash, repeat

I'm a Mo+ expert, an average programmer in terms of speed, and I started out being completely unfamiliar with the nopCommerce code.  A nopCommerce team expert knowledgeable of the architecture and best practices, being a good or better programmer in terms of speed, and having a working knowledge of Mo+ should be able to do this effort in less time.  Someone new to Mo+ (even an expert on the nopCommerce project) will likely take longer.

The Results (Benefits)

OK, what benefits did we get in return overall for this effort?  Here are the results for the unit tests:

  • Much Less Custom Code - 5796 lines of custom unit test code that had to be managed by hand is now managed by 467 lines of Mo+ code (line counts include comments and line breaks across all templates).  One master code template that outlines the framework for unit tests by entity generates and maintains 92 unit test classes (files) with no custom code required.  Custom unit tests can still be easily added, and about 5 custom tests that do very specific things remain active in the project.
  • Improved Quantity of Unit Tests - Increased the quantity of unit tests from 130 to 312 by:
    • Automatically finding and adding tests that were missing altogether
    • Adding a test for every reference (ex: an order test with referenced customer)
    • Adding a test for every collection (ex: a customer test with shopping cart items collection)
    • Easily adding a new category of tests (delete and verification)
  • Improved Quality of Unit Tests - Improved the quality of existing unit tests by:
    • Ensuring that all relevant properties, including references and collections, are added for testing
    • Ensuring that all relevant properties, including references and collections, are verified in at least one test
  • Codified Best Practices - Ensured that all unit tests rigorously follow nopCommerce unit test conventions and best practices.  Some custom unit tests strayed from the standard conventions.
  • Improved Ability to Augment Unit Tests - Provided a foundation for easily augmenting nopCommerce unit tests with new categories of tests and evolving best practices.  For example, tests with expected failures (such as missing references or required values) can be easily added.  The logic for generating tets values can be improved, and tests can easily include test values to (and beyond) maxlength.
  • Robustness to Change - With the Mo+ model and unit test templates in place, the unit tests can be automatically updated as additions and other changes to the nopCommerce database occur, maintaining the quantity, quality, and expected coverage of the unit tests.

There are additional benefits that the nopCommerce system and team can realize now that a model oriented foundation has been applied to the unit tests.  With the model in place, the benefit can spread outward to other layers in the system, where templates for those layers can yield similar results as above.  With the unit test templates in place, the benefit can spread outward to other systems (if the nopCommerce team plans to develop other things), where the templates (and best practices within them) can be applied to a different model for a completely different set of unit tests.

Creating the Model

Before we can make improvements to the persistence unit tests, we need to create a model.  The first order of business is creating the nopCommerce SQL Server database.  I did this by downloading and running the installer at nopcommerce.codeplex.com.  I called this database nopCommerceTest, you can name it whatever you want (if you have installed nopCommerce, go ahead and look through the database structure).

We create the model as a Mo+ solution named NopCommerce (with Mo+, open NopCommerceStep1.xml from the download, see the background section if you want more details on getting started with Mo+).  To load model information from the nopCommerce database, we need to add a database specification source.  In the Mo+ tree view below, you can see this database source, and the form contains the details for connecting to the database (you will have to change these details for your database).  We selected the MDLSqlModel template (from the sample pack, also in attached download) as a starting point to load information from the SQL Server database into the model (in Mo+ your model is updated when the solution file is opened or when executing the Compile Specification Source Data command at the solution level).

Image 2

Right out of the box, this template set gives us about 90-95% of what we want in the model, with the entities, relationships, basic properties, collections, and references to other entities.  See the Mo+ tree view and diagram below that shows some of this information.

Image 3

So, how do I figure how accurate this model is for nopCommerce?  I notice that the nopCommerce data model (and database) is built with Entity Framework code first, and the mappings are the best place to validate the model.  See the tree view below showing the organization of the mapping classes in the Nop.Data project, and the code for the CustomerMap class.

Image 4

C#
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);
    }
}

In comparing the nopCommerce source code with the model, I notice the following differences:

  1. All of the entities (such as Customer, Order) in the Mo+ model are organized by one feature called Domain.  The nopCommerce data related classes are consistently organized by other groupings (or features) such as Catalog and Customers (see tree view above).
  2. A few entities and references have different names (ex. Addres in model vs. Address in mapping).
  3. All collection names in the model end with "List" (such as OrderList in Customer), where in the mapping the names take a plural form (such as Orders).

We need to correct these differences before we start creating the model oriented unit tests.  We have two options:

  1. Custom Model Change - We can merely edit the model to make the changes we want.  Mo+ will keep track of these customizations, even as the source database changes.
  2. Automated Model Change - We can change the rules in a specification template to have Mo+ make the change to the model automatically.  You have complete control with Mo+ in determining how your model information is loaded.

For the features, I decided to do this by hand, since I didn't see an easy way to glean this information from the database schema, either through naming conventions or extended properties.  I created each of the features by hand and moved each entity to its corresponding feature.

For the entity and reference names, I also renamed these by hand since they are small enough in number.

The collection names however I want to address by changing a spec template.  Currently, a relationship level template called MDLCollectionName handles thes rules (MDLCollectionNameOrig in the attached download).  It looks like the following (there are no options to effectively display Mo+ as formatted code here, so Mo+ code will be shown as normal text or as images):

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

The modified code that pluralizes the names looks like the following (MDLCollectionName template in the attached download):

<%%:
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%%>
%%>

After reloading the model with the updated template, I found that a few collection names were still off, and I renamed these by hand.  The following diagram illustrates some of these model changes in the tree view (with Mo+, open NopCommerceStep2.xml from the download).  In the tree view, note that some of the elements are in darker text.  These elements contain custom changes.  The form shows the BlogPost entity, and illustrates that the feature was changed (Feature label is in darker text).

Image 5

At this point the model looks good to me from a visual inspection, and, now is a good time to move on.  Undoubtedly I have missed a few details in the model, but those will show themselves (usually as compilation errors) as I generate and integrate the unit tests.

Creating Initial Unit Test Templates

Creating a model oriented template for a legacy system (and often in general) must start with a good example pattern in practice.  A great majority of the persistence tests in the Nop.Data.Tests project follow a standard convention for saving an entity (db record), getting it, and verifying the results.  There are tests for saving and verifying the entity itself, and also for saving the entity with references and collections.

Establishing the Example Pattern

Creating a model oriented template for a legacy system (and often in general) must start with a good example pattern in practice.  A great majority of the persistence tests in the Nop.Data.Tests project follow a standard convention for saving an entity (db record), getting it, and verifying the results.  There are tests for saving and verifying the entity itself, and also for saving the entity with references and collections.

So, I selected the BlogPostPersistenceTests as a good example to start with, since it's not too big and has most of the patterns I'm looking for.

C#
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)
            };
        }
    }
}

Notice that this class has a basic Can_save_and_load_blogPost test and a Can_save_and_load_with_blogComments test for the BlogComments collection.  I definitely want to use these two methods as patterns.

I also know that there are tests for an entity and a corresponding nullable reference, but that example isn't here.  It does exist in the OrderPersistenceTests, such as Can_save_and_load_order_with_shipping_address, which tests an order with its shipping address reference:

C#
[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");
 }

I will use these three test methods as patterns for the model oriented unit tests.

Creating a Raw Text Template

To begin creating a template from these best practice examples, we just create a "raw text" Entity level code template in Mo+ that contains the BlogPostPersistenceTests class and the extra method from OrderPersistenceTests (plus a couple related GetTest methods).  In the portion of the template shown below, the orange text gets generated as literal text.  There is no model data inserted into it at this stage (see NopDataTestStep1 template in the download).

Image 6

Running this template in the Mo+ debugger (F5) emits the following C# code:

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()
            };
        }
    }
}

Adding Basic Model Information (Entities and Features)

From this basic raw text template, we go through a series of steps to massage the template, replacing raw text with model data, adding other logic that determines when and how to generate text, and breaking apart more complicated things as we encounter them into more manageable pieces.

It's always a good idea to start simple.  For an entity level template such as this, the first step is replacing entity and parent level feature information with actual model data.  In our raw text, the feature is Blogs, and the entity is BlogPost and Order (since we grabbed content from two different tests).  We perform a search and replace of these elements with model data (FeatureName and EntityName).  In the portion of template text illustrated below, the feature name is inserted at line 19, and entity name is inserted at lines 22 and 25, etc. (see NopDataTestStep2 template in the download).  Model elements appear as teal text in the template.

We also need to generate references to the actual data classes in Nop.Core.Domain.  To keep it simple right now, we blindly create a reference for every feature to the corresponding Nop.Core.Domain classes (lines 12-16 below).  Statements appear as blue text in the template.

When you debug a template in Mo+, a random element from the model is chosen (in this case a random entity).  You can also set breakpoints and type any number of watch expressions to see what is going on, as illustrated below.  A special Text property lets you see what text has been generated when the breakpoint is reached.

Image 7

The following is an example of a generated persistence tests class (in this case for a ForumGroup).

C#
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()
            };
        }
    }
}

Adding Deeper Model Information (Properties, References, Collections)

You should be able to see that the property names and some of the method names for ForumGroupPersistenceTests above are not correct.  We still have a lot of literal text in our template.

First, let's add basic property information, we iterate to set and verify the property values (just putting in "some value" for the test values for now (see lines 32-36 and 47-51 below).

Image 8

Then, we iterate through our collections to generate the save and load by collection methods, adding basic collection information.

Image 9

Then, we iterate through our nullable references to generate the save and load by reference methods, adding basic entity reference information.

Image 10

Following shows an example generated unit test (BlogPost) from our updated template (see NopDataTestStep3 template in the download).

C#
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()
            };
        }
    }
}

Divide and Conquer (Adding GetTest Methods and More)

Notice in our template and in the nopCommerce persistence unit tests in general that often there are inline GetTest methods (such as GetTestCustomer) to get test data for a corresponding reference.  Having these inline methods is a fair amount of duplicate code, plus it makes things a little more complicated in our template to generate those inline, so I want to get rid of that duplication.

I chose to handle this by adding a static GetTest method to the persistence test, to have one place to get a test instance of that type (ex. CustomerPersistenceTests will have a GetTestCustomer method).  Maybe I should have put these methods in a separate helper class, but that change can easily be done later.

As part of the divide and conquer strategy, I created this test method as a separate entity level template (see NopDataTestGetTestMethodStep4 template in the download).  Below are the details of the template that sets up this method.

Image 11

Because Mo+ templates are truly model oriented, you can use any template you create just like any other built in model property from another template, even in expressions.  If we wanted to create that helper class to contain all of the GetTest methods, we can very easily use this template for generating those methods.  This really gives you the power to divide and conquer effectively.

In our main template, we just generate the contents of our new GetTest method (at line 27 below), and we continue to replace literal text with deeper information from the model.

Image 12

Following shows an example generated unit test (ProductManufacturer) from our updated templates (see NopDataTestStep4 template in the download).

C#
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();
        }
    }
}

I know there are additional details to resolve with these templates, but at this point I want to try them out in the unit test project.  So, let's integrate these tests into our project and see what happens!

Integrating the Model Oriented Unit Tests

For integrating our work so far, how do we set up our unit test templates to generate code for our project?  With Mo+, it's just a matter of defining which entities we should generate these unit tests for, and where/when the unit test files should be updated.

Solution Template

With Mo+, it all begins at the solution level.  Below is the output section of a solution level template, GenerateProjects.  The output section of a template specifies where, when, and how to generate your output, giving you complete control over code generation, which is especially essential when you are part of a team and cannot afford to have everything regenerated every time.

This template basically goes through each project, and outputs whatever content is specified for each project in the solution.  The Template output property (in magenta text) is a special property that executes the actual template associated with the project.  If you never need to manage any files at the solution level, this is all you ever need for a solution level template (see GenerateProjects in download).

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

// generate code for each defined project
foreach (Project)
{
    CurrentProject = Project
    
    // output project contents
    <%%>Template%%>
}
%%>

We need to let Mo+ know to execute this template when generating code.  We do this by selecting this template in the basic solution information.

Image 13

Project Template

We need a project level template to determine which entities we want to generate unit tests for.  In the output section of this template, we choose to output (generate) our unit test (the output of the NopDataTest unit test template) for each Entity that does not have a tag of Ignore (see NopDataTests template in download).  The progress statements are merely used to increment the progress bar when we generate code (we defined the amount of work for progress in the solution level template).

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

To use this template, we need to define a project in our model.  Below, we created a project called Nop Data Tests, and selected our NopDataTests project level template.  That's it, now our solution level template will find this template to generate our unit tests!

Image 14

Creating the Unit Tests in the Nop.Data.Tests Project

To complete our integration steps, we need to add our generated unit tests to the Nop.Data.Tests project.  When working on large complex system, particularly in team environments, you can't affort to have all of your model oriented regenerated every time.  Mo+ is the only technology that gives you complete control in determining, where, when, and how to updated your generated documents.

Following is the output section of the NopDataTest template that determines your output:

  • Where -The variable filePath holds the information as to where the unit test file is to be created or updated.  We want the unit test files in the same project and feature folder as the existing unit tests.  We choose to add _G to the file name as a convention to easily view which files are generated.
  • When - When to update generated files is very important.  The conditions chosen for each unit test are:
    • If the file does not exist, generate it, otherwise update the file if:
      • The file has an indication that it can be regenerated.  A developer could choose to change the status comment at the top of the file to prevent the file from being regenerated (such as if there are too many exceptions for a particular file to want to regenerate it).
      • And the file has core generated code that has changed.  Mo+ allows you to define areas to ignore, so for example you can choose not to regenerate a file if the differences are only because who generated the updates or when.  Mo+ also allows you to define areas to protect, so you can insert custom code inside of generated code, and only choose to regenerate code (keeping your custom code) when code outside your protected areas has changed.  You can see these configuration settings in the content area of the GenerateProjects solution template.
  • How - Here, we just choose to create or update the unit test file when the conditions are met (using the update statement).  We could choose to do other things such as add (or remove) generated unit tests to the unit test project automatically, or deleting generated files that are no longer relevant.  Many of the templates in the Mo+ sample packs do just that, but I chose to keep the logic here simple and let the developer add or remove the generated tests in the project manually.

<%%:
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)
}
%%>

Generating the Unit Tests

Once the above integration pieces are set up, it's just a matter of generating the unit test files (in Mo+ with the Update Output Solution command at the solution level) and including them in the Nop.Data.Tests project in Visual Studio.

Image 15

Debugging and Final Model Oriented Templates

When you first integrate your model oriented code on the first pass, undoubtedly you will get compilation errors, followed by runtime errors, and I certainly did.  These issues usually quickly point to where you need to make corrections in either your model or your templates.  I am not going to walk you through all of the detailed changes I made, but following are some of the main changes I made to the templates while debugging (and also making a couple of improvements):

  • Model Issues - I found that some entities and collections in the model had no corresponding mapping, even though corresponding info in the database is there.  So, I just tagged these elements as "Ignore" and skipped generating unit tests for those.  I also needed to rename or add a couple additional collections.
  • Test Values - We knew at the outset that the "some value" test value wasn't going to work.  So I created a separate template to create decent test values by data type (see TestValue property template in attached download).
  • References - We knew that we only want to create the references we need to other project areas, so I created a separate template to output only needed references (see NopDataTestReferences in attached download).
  • Divide and conquer by test type - I found it easier to break down each type of test as a separate template so I could separate the logic of when to create the test methods from the logic of creating the method contents (see NopDataTestSaveAndLoadMethod entity template, NopDataTestSaveAndLoadWithCollectionMethod collection template, and NopDataTestSaveAndLoadWithReferenceMethod entity reference templates in download).
  • More tests by reference - I added additional tests after realizing that some save and load tests were not fully testing the values of required references.  So, I generated a test by reference for all references, not just nullable (optional) ones (see logic change in NopDataTest entity template in download).
  • Delete tests - I added additional tests to verify that deletes work as expected (see NopDataTestSaveAndDeleteMethod in attached download).

Below is the contents of the NopDataTest template, which shows the divide and conquer approach of utilizing other templates for test methods, and other things such as adding comments to the tests.

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

<%%-
///--------------------------------------------------------------------------------
/// <summary>This class is used to perform persistence data unit tests
/// on %%><%%=EntityName%%><%%- 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>%%><%%=USER%%><%%-</CreatedByUserName>
/// <CreatedDate>%%><%%=NOW%%><%%-</CreatedDate>
/// <Status>Generated</Status>
///--------------------------------------------------------------------------------
namespace %%><%%=Project.Namespace%%><%%-.%%><%%=FeatureName%%><%%-
{
    [TestFixture]
    public partial class %%><%%=EntityName%%><%%-PersistenceTests : PersistenceTest
    {%%>
    //
    // add the GetTest static method
    //
    <%%=NopDataTestGetTestMethod%%>
     //
     // add the save and load persistence test
     //
     <%%=NopDataTestSaveAndLoadMethod%%>
     //
     // create the save and delete persistence test
     //
     <%%=NopDataTestSaveAndDeleteMethod%%>
     
        foreach (EntityReference)
        {
            //
            // create save and load persistence test with addition of referenced object
            //
            <%%=NopDataTestSaveAndLoadWithReferenceMethod%%>
        }
       foreach (Collection  where Tags.Contains("Ignore") == false && ReferencedEntity.Tags.Contains("Ignore") == false)
        {
            //
            // create save and load persistence test with addition of collection
            //
            <%%=NopDataTestSaveAndLoadWithCollectionMethod%%>
        }
        <%%-
    }
}
%%>
%%>

Below is the contents of the NopDataTestSaveAndDeleteMethod for testing delete.

<%%:
     <%%-
     
        [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();
        }%%>
%%>

Below is a final generated unit test file example (BlogPostPersistenceTests).

C#
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));
        }
    }
}

Review all of the Mo+ code templates in the attached download to look at additional details, and review all of the Nop.Data.Tests generated unit tests in the attached download.

Going Forward with nopCommerce

I plan to get in touch with the nopCommerce development team and present to them the results outlined in this article.  I hope that the development team will choose to adopt Mo+, at least for the unit tests, and that I can join their project as a contributor and help them on additional areas as well.  I will update this article based on feedback from the nopCommerce team.

When Should You Apply MOD to Your Legacy System?

MOD gives you the flexibility to apply as little or as much model oriented code to manage your legacy system as you choose.  You have to weigh the cost of ramping up on the MOD technology and doing the model oriented work versus the immediate and downstream benefit you will get out of it.  Here are some things to consider for applying MOD to legacy systems:

  • Do you have a model, or is it worth creating one? - First of all, you have to weigh the cost of providing a model for MOD versus the benefit.  Do you have any kind of model that represents the structure and behavior of your system?  If not, do you see a general value for having such a model for communication, planning, and other purposes?  If the answer to both of these questions is no, and there is a good bit of effort in determining what the model is, than MOD is probably not right for your legacy system.  You can choose to create a model from scratch, using Mo+ or another technology.  If your system has a database such as SQL Server or MySQL, or if you have UML or other models that can be output as XML, your modeling effort will likely be low as needed for downstream development.  Mo+ is unique in that it has a specification template feature that is specifically geared towards reducing your modeling effort if you already have a source.  You have COMPLETE control over how you want to translate your source model for development purposes (a simple example of naming collections to nopCommerce practices was demonstrated above).
  • Can you envision model oriented patterns in your system? - This may be a harder question to answer, but you should envision certain model oriented patterns in your system and see the potential benefit before applying MOD.  For example, say that your model includes Customer, Product, and Order, and you have customer, product, and ordering UI screens.  Do you see similarities in how data is presented in these screens (how customer name is presented on customer screen vs. how product name is presented on product screen), or in how similar actions are taken (adding a customer or adding an order being similar but with different data)?  If the answer is yes, you have envisioned a model oriented pattern!  Team best practices are important in establishing good model oriented patterns.  Good best practices that can be codified will make applying MOD that much easier.
  • What immediate improvements do you need? - Before you make changes to your legacy system with MOD (or in general), of course you have improvements in mind.  Are you planning major feature changes?  Do you need to fill in gaps to complete existing functionality or make it more robust?  Are there general inconsistencies in your source code that you need to rectify?  If the answers to any of these questions are yes, it is a good time to consider MOD.
  • How much custom code should you manage with model oriented code? - This very much depends on your system, but you have to weigh the cost of creating and maintaining model oriented code (templates) versus the benefit of savings in your custom coding efforts.  Building a very complex template to maintain one class file is probably not worth the effort.  Building a fairly complex template to maintain 100 class files, allowing you to insert custom code for your 10 really complicated classes is very much worth the effort.  As an initial rule of thumb, I would say that if a new template you create can manage 10 things in your custom code, go for it!  If you can reuse downloaded or your own stable templates, go for it, even if it manages only 1 thing in your custom code!  Mo+ is unique in its truly model oriented approach to code generation, allowing you to divide and conquer complexity so that you can manage more of your code in a model oriented way and seemlessly integrate your model oriented and custom code (some examples of this was demonstrated above).  Another point to make here is that your goal shouldn't necessarily be to have as much model oriented (generated) code as possible.  If in your efforts you find that you can replace your model oriented code with robust, concise, model independent custom code, do it!
  • Do you plan to refactor code to new technologies or best practices? - If you need to make a lot of change to your legacy code to migrate to a new language or technology, or evolve a lot or your code to your updated best practices, this is a great opportunity to apply MOD!  You can debug and your code based on model oriented patterns much more quickly than your other custom code.  And your model oriented code will in the future be much easier to change when your technologies and best practices further evolve.

In Summary

Hopefully this article has provided you with some food for thought in considering model oriented development when making changes and enhancements to your legacy systems, especially enterprise systems with databases such as nopCommerce.

In addition, I hope you try Mo+ and the Mo+ Solution Builder to fully utilize incorporating a model oriented approach into your development efforts. The free, open source product is available at moplus.codeplex.com. In addition to the product, this site contains sample packs for building more complete models and generating complete working applications. Video tutorials and other materials are also available at this site. The Mo+ Solution Builder also contains extensive on board help.

Become a Member!

The Mo+ community gets additional support, and contributes to the evolution of Mo+ via the web site at https://modelorientedplus.com.  Being a member gives Mo+ users additional benefits such as additional forum support, member contributed tools, and the ability to vote on and/or contribute to the direction of Mo+. Also, we are running contests for members, where you can win $ for developing solutions using Mo+.

If you are the least bit interested in efficient model oriented software development, please sign up as a member. It's free, and you won't get spam email!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)