Try our conversational search powered by Generative AI!

MilosR
Apr 26, 2024
  327
(1 votes)

Optimizely Unit Testing Using CmsContentScaffolding Package

Introduction

Unit tests shouldn't be created just for business logic, but also for the content and rules defined for content creation (available content types, validations etc.). That way Optimizely solution becomes resilient to content type and rule changes.

Usually there is a local development DB or test DB that is used continuously and, as test content is created in Optimizely and backend development is continuously changing content types and validation rules, it rarely gets verified as if content is created from scratch. That can lead to false thinking that test content represents realistic picture of what client will be able to create, but those issues will start popping up on staging environment as soon as client starts with content creation.

The benefit you get with content unit tests is verification of desired site structure and validation rules as soon as change is introduced and always starts with empty DB from scratch, verification that controller output model contains all data needed. In order to easily outline content structure there is a library called CmsContentScaffolding that uses simple fluent syntax. It can create complete site structure, easily create large number of pages and prepare content for testing.

CmsContentScaffolding Package

Added as a Nuget package and registered service dependency using: services.AddCmsContentScaffolding();, followed with IApplicationBuilder extension method appBuilder.UseCmsContentScaffolding(builderOptions: o => {}, builder: b => {});. There are two sections: builderOptions (provides global options like language used, site host, site name, publish or just create page drafts, users and roles...) and builder that exposes methods for content creation. You can create multiple sites by using another section of appBuilder.UseCmsContentScaffolding(builderOptions: o => {}, builder: b => {});. Builder have two main methods UseAssets(ContentReference.SiteBlockFolder) and UsePages(ContentReference.RootPage) which serves as a starting points from where content creation starts.

Additionally, there is a package with helper methods for fake texts, images and videos, called CmsContentScaffolding.Shared.Resources.

UseCmsContentScaffolding method has many options and can look like this:

app.UseCmsContentScaffolding(
    builderOptions: o =>
    {
        o.Language = CultureInfo.GetCultureInfo("sr");
        o.StartPageType = typeof(StartPage);
        o.BuildMode = BuildMode.Append;
    },
    builder: b =>
    {
        var teaser2Ref = ContentReference.EmptyReference;
        var teaser3Ref = ContentReference.EmptyReference;
        var articlePageRef = ContentReference.EmptyReference;

        b.UseAssets(ContentReference.SiteBlockFolder)
        .WithFolder("Folder 1", l1 =>
        {
            l1
            .WithFolder("Folder 1_1", l2 =>
            {
                l2.WithBlock<TeaserBlock>("Teaser 1", out teaser2Ref, x =>
                {
                    x.Heading = "Teaser 1 Heading";
                    x.Text = ResourceHelpers.Faker.Lorem.Paragraph();
                    x.Image = PropertyHelpers.GetOrAddMedia<ImageFile>("Image 2", ".png", ResourceHelpers.GetImageStream());
                });
            })
            .WithMedia<VideoFile>(x => x.Name = "Test video", ResourceHelpers.GetVideoStream(), ".mp4")
            .WithBlock<TeaserBlock>("Teaser 3", out teaser3Ref, x =>
            {
                x.Heading = "Test";
                x.Text = ResourceHelpers.Faker.Lorem.Paragraph();
                x.Image = PropertyHelpers.GetOrAddMedia<ImageFile>("Image 2", ".png", ResourceHelpers.GetImageStream());
            });
        })
        .WithContent<ContentFolder>(x => x.Name = "Folder1")
        .WithContent<ImageFile>(x => x.Name = "Image 1");

       b.UsePages(ContentReference.RootPage)
        .WithStartPage<StartPage>(p =>
        {
            p.Name = "Home Page";
            p.MainContentArea
            .AddExistingItems(teaser2Ref, teaser3Ref)
            .AddItem<TeaserBlock>("Start Page Teaser", b =>
            {
                b.Heading = ResourceHelpers.Faker.Lorem.Slug();
                b.Text = ResourceHelpers.Faker.Lorem.Paragraph();
                b.Image = PropertyHelpers.GetOrAddMedia<ImageFile>("Image 2", ".png", ResourceHelpers.GetImageStream());
            });
        }, CultureInfo.GetCultureInfo("sv"), t =>
        {
            t.Name = "Start Page [SV]";
        }, l1 =>
        {
            l1
            .WithPage<ArticlePage>(out articlePageRef, p =>
            {
                p.Name = "article1";
                p.MetaTitle = ResourceHelpers.Faker.Lorem.Slug();
                p.TeaserText = ResourceHelpers.Faker.Lorem.Paragraph();
                p.MainBody
                .AddStringFragment(ResourceHelpers.Faker.Lorem.Paragraphs())
                .AddContentFragment(PropertyHelpers.GetOrAddMedia<ImageFile>("Image 1", ".png", ResourceHelpers.GetImageStream()))
                .AddStringFragment(ResourceHelpers.Faker.Lorem.Paragraphs());
                p.PageImage = PropertyHelpers.GetOrAddMedia<ImageFile>("Image 1", ".png", ResourceHelpers.GetImageStream());
                p.MainContentArea
                .AddItem<JumbotronBlock>("Jumbotron Block", b =>
                {
                    b.ButtonText = ResourceHelpers.Faker.Lorem.Slug();
                    b.ButtonLink = new Url(ResourceHelpers.Faker.Internet.Url());
                    b.Heading = ResourceHelpers.Faker.Lorem.Slug();
                })
                .AddItem<ImageFile>(options: i =>
                {
                    i.Name = "Test Image";
                    i.ContentLink = PropertyHelpers.GetOrAddMedia<ImageFile>("Image 1", ".png", ResourceHelpers.GetImageStream());
                })
                .AddExistingItem(teaser3Ref);
            })
            .WithPages<ArticlePage>(p =>
            {
                p.Name = "Article 33";
                p.MetaTitle = ResourceHelpers.Faker.Lorem.Slug();
                p.TeaserText = ResourceHelpers.Faker.Lorem.Paragraph();
                p.MainBody.AddStringFragment(ResourceHelpers.Faker.Lorem.Paragraphs(10));
                p.MainContentArea.AddExistingItem(teaser2Ref);
            }, 100);
        })
       .WithPages<ArticlePage>(p =>
        {
            p.Name = ResourceHelpers.Faker.Lorem.Slug(2);
            p.MainContentArea
            .AddItems<TeaserBlock>(b =>
            {
                b.Heading = ResourceHelpers.Faker.Lorem.Slug();
                b.Text = ResourceHelpers.Faker.Lorem.Paragraph();
                b.Image = PropertyHelpers.GetOrAddMedia<ImageFile>("Image 1", ".png", ResourceHelpers.GetImageStream());
            }, 10);
        }, 10)
    });

It allows you to create assets structure, reuse created content by fetching it's references, create translated versions, create sub items without limit, create many content items of the same type using total parameter which can be any number between 1 and 10000, multiple sites with specific users and roles assigned etc.

Package is open source and can be found on this URL: https://github.com/milosranko/CmsContentScaffolding

Unit Test Fundamentals

We can identify five important points when it comes to creating unit tests:

  1. Defining a unit test
  2. Should be automated and repeatable
  3. Should be easy to implement and read using AAA pattern (Arrange, Act, Assert)
  4. Should be fully isolated
  5. Cleanup resources after tests are finished

We will first initialize test using dedicated unit test DB (special SQL Server user should be created with sa rights to create and delete DB), then we will seed content using CmsContentScaffolding, write tests and clean up resources (including DB) after tests are finished.

Unit test can be performed using contentLoader to get desired content and verify it is created invoking controller action.

Example

In this example we will see how test method can look like when implemented using triple A principle and when controller method is invoked. We verify that the output view model is having expected state.

[TestMethod]
public void StartPageController_ShouldReturnIndexViewModel()
{
    //Arrange
    var contentLoader = ServiceLocator.Current.GetRequiredService<IContentLoader>();
    var siteDefinitionRepository = ServiceLocator.Current.GetRequiredService<ISiteDefinitionRepository>();
    var siteDefinition = siteDefinitionRepository
        .List()
        .Where(x => x.GetHosts(CultureInfo.GetCultureInfo("sr"), false).Any())
        .Single();
    var startPage = contentLoader.Get<StartPage>(siteDefinition.StartPage);
    var _controller = new StartPageController();

    //Act
    var res = (ViewResult)_controller.Index(startPage);

    //Assert
    Assert.IsNotNull(res);
    Assert.IsInstanceOfType(res.Model, typeof(PageViewModel<StartPage>));
    Assert.IsNotNull(((PageViewModel<StartPage>)res.Model).CurrentPage.MainContentArea);
}

Conclusion

Constant verification of implemented code is crucial for continuous software development. In Optimizely CMS framework content and site structure are crucial components and must be maintained and constantly verified in order to avoid unwanted issues when you less expect them. I have used CmsContentScaffolding library on last project and found it very helpful, either as a unit test helper or to quickly scaffold site structure and perform manual tests and investigate bugs.

I would like to ask you to check it out and leave a comment on this blog post.

https://github.com/milosranko/CmsContentScaffolding

Appendix

CmsContentScaffolding package can be also used to outline products catalog in Optimizely Customized Commerce solution.

Example:

app.UseCmsContentScaffolding(
    builderOptions: o =>
    {
        o.SiteName = "Demo";
        o.Language = CultureInfo.GetCultureInfo("en");
        o.SiteHost = "https://localhost:5000";
        o.BuildMode = BuildMode.Append;
        o.StartPageType = typeof(StartPage);
    },
    builder: b =>
    {
        b.UseAssets(referenceConverter.GetRootLink())
        .WithContent<CatalogContent>(x =>
        {
            x.Name = "Catalog 1";
            x.DefaultCurrency = "EUR";
            x.DefaultLanguage = "en";
            x.WeightBase = "kgs";
            x.LengthBase = "cm";
        }, l1 => l1.WithContent<FashionNode>(x => x.Name = "Men", l2 =>
                    l2.WithContent<FashionNode>(x => x.Name = "Shoes", l3 =>
                        l3.WithContent<FashionProduct>(x => x.Name = "Product 1", l4 =>
                            l4
                            .WithContent<FashionVariant>(v => v.Name = "Variant 1")
                            .WithContent<FashionVariant>(v => v.Name = "Variant 2"))
                        .WithContent<FashionProduct>(x => x.Name = "Product 2")
                        .WithContent<FashionProduct>(x => x.Name = "Product 3")
                 ).WithContent<FashionNode>(x => x.Name = "Accessories", l3 =>
                    l3
                    .WithContent<FashionProduct>(x => x.Name = "Product 1")
                    .WithContent<FashionProduct>(x => x.Name = "Product 2")
                    .WithContent<FashionProduct>(x => x.Name = "Product 3")
                 )
            )
        );
    });
Apr 26, 2024

Comments

Please login to comment.
Latest blogs
Do not upgrade to EPiServer.CMS.Core 12.21.4 without reading this!

Todays update of EPiServer.CMS.Core 12.21.4 alters default sort order in an unexpected way, when you are working with muligple languages and have...

Tomas Hensrud Gulla | May 14, 2024 | Syndicated blog

Upgrade Optimizely CMS From v11 to v12

Why Upgrade? There are many improvements implemented in version 12, but most importantly is that the whole framework is migrated from .Net Framewor...

MilosR | May 13, 2024

Configured Commerce - Infrastructure Updates Ahoy!

I'm very happy to share an important milestone - we no longer have any customers in our legacy v1 environment!  This means that the Configured...

John McCarroll | May 10, 2024

A day in the life of an Optimizely Developer - Enabling Opti ID within your application

Hello and welcome to another instalment of A Day In The Life Of An Optimizely developer, in this blog post I will provide details on Optimizely's...

Graham Carr | May 9, 2024

How to add a custom property in Optimizely Graph

In the Optimizely CMS content can be synchronized to the Optimizely Graph service for it then to be exposed by the GraphQL API. In some cases, you...

Ynze | May 9, 2024 | Syndicated blog

New Security Improvement released for Optimizely CMS 11

A new security improvement has been released for Optimizely CMS 11. You should update now!

Tomas Hensrud Gulla | May 7, 2024 | Syndicated blog