Theme:

Create an Umbraco package test environment using PowerShell

When you create a package you eventually need to test it in a real site. Repeatedly creating test sites can be a time consuming process if done manually. In this article I show you how I have automated the creation of test sites that reference my package code, so I can quickly start testing or debugging the next cool feature for a package in a real Umbraco site.

A little background

Creating a test site for your package requires a lot of steps.

  • Install the correct version of Umbraco.
  • Install a starterkit so we have some content.
  • Configure the site. The starterkit is a good start but most of the times we need to use our own datatypes or include extra content, members, or whatever we need.
  • Include Package code. I prefer referencing my source code from the test site instead of having a test site in my package solution. This is because I want to have a site in an initial state when I debug something for a client. So we need a solution file and to reference my package source code.

If you are like me, you need to do this a lot of times for multiple versions of Umbraco, testing of features, testing a release, or going back to an initial state when a bug report comes in.

Or if you are really like me you did something stupid and broke the whole site and need to start over again.

In this article I will show you how this can be automated using a small PowerShell script that is using dotnet CLI and the power of the Umbraco template.

What will we create

Some people might know me from the package called Admin Reset in the past. People forget their passwords all the time, so I have created a package called Member Reset, and am using that as a demo in this article.

Member reset dashboard

Create the site using dotnet CLI and Umbraco unattended install option

To create the site I have created a PowerShell script that allows you to set some variables for this specific package. Then it generates a test site using Umbraco's unattended install option. This allows us to generate a site without any user interaction.

Since we are using LocalDB we don't need to configure a database as well. I could have used SQLite but want to stay as close to production ready as possible, therefore SQL Server.

And I also don't worry about the security of the username and password in this script since test and debug sites will never see the day of light.


#Set Variables
$sitename = "24DaysTestSite"
$productlocation = "F:\umbraco\Demo\DevopsTalkGitHub\"
$projects = "DevOpsTalk.Core\DevOpsTalk.Core.csproj", "DevOpsTalk.StaticAssets\DevOpsTalk.StaticAssets.csproj", "DevOpsTalk.Web\DevOpsTalk.Web.csproj", "DevOpsTalk.AddTestData\DevOpsTalk.AddTestData.csproj"

#Create new Umbraco Site
dotnet new install Umbraco.Templates::12.3.0
dotnet new umbraco -n $sitename  --connection-string "Server=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=true"

cd $sitename 
#Set info for installer
Set-Item Env:\UMBRACO__CMS__GLOBAL__INSTALLMISSINGDATABASE true
Set-Item Env:\UMBRACO__CMS__UNATTENDED__INSTALLUNATTENDED true
Set-Item Env:\UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERNAME "Soeteman Software Test User"
Set-Item Env:\UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL "testuser@soetemansoftware.nl"
Set-Item Env:\UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD "test123456"

Create a solution file

In the next step we will create a solution file for this project, add the newly created project file, and install a starterkit for some content in the site. In this step we also reference the source code of the real package code as well to ensure we can debug that code immediately when starting the solution.

As mentioned before I prefer to have my core package code in a separate folder, isolated from this test site. This allows me to just delete the complete website and start over again without having to fear I deleted some real package code that was not yet checked in Source Control.

In the initial script above I specified the path to my real source code in the $productlocation variable and the projects to include in the $projects variable. The part of the script below ensures those projects are added in the solution and are referenced by our new test site project.


#Create solution and add project and Starterkit
dotnet new sln
dotnet sln add $($sitename+".csproj")
dotnet add package Umbraco.TheStarterKit --version 12.0.0

#Add other projects to the solution
foreach ($project  in $projects ) 
{
	dotnet sln add $($productlocation + $($project))
	dotnet add $($sitename+".csproj") reference $($productlocation +$($project))
}

Build and run the solution

Now we have all pieces in place we can build and run the site. It's really nice to watch this process since it exactly shows what you manually had to do without this script. After about 30 seconds (most of the time is taken by missing database messages) all is ready and you can use the site.


#build site including dependencies
dotnet build

#start site
dotnet run

Modify starterkit

Ok so we have our new shiny test site, but one thing the PowerShell script did not do was to modify the Umbraco StarterKit to create some members, and/or add a login option on the text template to really test the functionality of this package. We don't want to do this manually every time.

Maybe you already noticed, one project we also reference is DevOpsTalk.AddTestData. This project contains a PackageMigration that gets triggered on startup once to create some members and replace the default.

First we create a PackageMigrationPlan and a PackageMigration to call the actual ITestDataService service during install. In the Migrate() method of the PackageMigration we call the AddTestData() method of the ITestDataService.


public class DevOpsTalkTestDataMigrationPlan : PackageMigrationPlan
 {
     public DevOpsTalkTestDataMigrationPlan() : base("DevOpsTalk") { }

     protected override void DefinePlan()
     {
         To<DevOpsTalkTestDataMigrations>("DevOpsTalkTestDataModification");
     }
 } 

public class DevOpsTalkTestDataMigrations : PackageMigrationBase
 {
     private readonly ITestDataService _testDataService;

     public DevOpsTalkTestDataMigrations(IPackagingService packagingService,
         IMediaService mediaService,
         MediaFileManager mediaFileManager,
         MediaUrlGeneratorCollection mediaUrlGenerators,
         IShortStringHelper shortStringHelper,
         IContentTypeBaseServiceProvider contentTypeBaseServiceProvider,
         IMigrationContext context,
         IOptions<PackageMigrationSettings> packageMigrationsSettings,
         ITestDataService testDataService) : base(packagingService, mediaService, mediaFileManager, mediaUrlGenerators, shortStringHelper, contentTypeBaseServiceProvider, context, packageMigrationsSettings)
     {
         _testDataService = testDataService;
     }

     protected override void Migrate()
     {
         _testDataService.AddTestData();
     }
 }

To give you some insight how the test data is actually generated I've added the implementation of the actual TestDataService below as well.

  • First we add some members using the AddMembers() method.
  • Then we copy the modified views that we included as an embedded resource to disk. This replaces the actual views of the Starterkit.

using Microsoft.Extensions.Hosting;
using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Services;

namespace DevOpsTalk.AddTestData.Services
{
    internal class TestDataService : ITestDataService
    {
        private readonly IMemberService _memberService;
        private readonly IHostEnvironment _hostEnvironment;

        public TestDataService(IMemberService memberService, IHostEnvironment hostEnvironment)
        {
            _memberService = memberService;
            _hostEnvironment = hostEnvironment;
        }

        public void AddTestData()
        {
            AddMembers();
            EnsureViewsCopiedToDisk();
        }

        /// <summary>
        /// Adds Some test members
        /// </summary>
        private void AddMembers()
        {
            //Fake members to add
            var names = new List<string> { "Jillian Kent", "Rhonda Henry", "Elise Mendez", "Everett Kane", "Tracy Petty", "Shannon Farmer", "Emanuel Hancock", "Sam Moore", "Ralph Dunlap", "Sal Pham", "Tamra Larsen", "Rubin Cantu", "Sanford Lawrence", "Donald Tanner", "Brett May", "Florine Stein", "Val Mccarty", "Josephine Patel", "Kent Nicholson", "Willy Perry", "Georgette Chaney", "Brain Washington", "Kim Frank", "Katy Ochoa", "Emilio Herman", "Shawn Perez", "Reyna Edwards", "Gordon Knight", "Charlene Brown", "Gary Obrien", "Jared Vasquez", "Brooke Stevenson", "Haley Savage", "Timothy Murray", "Stella Vargas", "Flossie Tucker", "Autumn York", "Brenda Holmes", "Douglas Gallagher", "Art Mason", "Stanford Benitez", "Eileen Blackburn", "Wilda Jefferson", "Florencio Roberson", "Erica Hahn", "Lionel Osborne", "Preston Michael", "Amber Hopkins", "Angelica Ferrell", "Angeline Cooper" };

            foreach (var name in names)
            {
                var email = name.Replace(" ", string.Empty) + "@contoso.com";
                var member = _memberService.CreateMember(email, email, name, "member");
                _memberService.Save(member);
            }
        }

        /// <summary>
        /// Copies login functionality and modified content page
        /// </summary>
        private void EnsureViewsCopiedToDisk()
        {
            var currentAssembly = this.GetType().Assembly;
            var assemblyName = currentAssembly.GetName().Name ?? string.Empty;

            foreach (var manifestResource in currentAssembly.GetManifestResourceNames())
            {
                var manifestResourceName = manifestResource.Replace(assemblyName, string.Empty);

                //Modify Resource name to a filename
                var manifestParts = manifestResourceName.Split('.', StringSplitOptions.RemoveEmptyEntries);
                manifestParts = manifestParts.Skip(1).ToArray();
                var folder = string.Join("/", manifestParts.Take(manifestParts.Length - 2));
                var filename = string.Join(".", manifestParts.Skip(manifestParts.Length - 2));

                var folderOnDisk = _hostEnvironment.MapPathContentRoot(folder);

                if (!Directory.Exists(folderOnDisk))
                {
                    Directory.CreateDirectory(folderOnDisk);
                }

                var fileOnDisk = Path.Combine(folderOnDisk, filename);

                //Replace existing files
                using var stream = currentAssembly.GetManifestResourceStream(manifestResource);
                using var fileStream = new FileStream(fileOnDisk, FileMode.Create, FileAccess.Write);
                stream?.CopyTo(fileStream);
            }
        }
    }
}

References

I hope this gave you some insight how you can quickly setup a test site for your package that you can use over and over again.

Below are references to all detailed documentation and source code.

Happy Holidays!