Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Kieron McIntyre 79 posts 293 karma points
    Jan 03, 2013 @ 10:36
    Kieron McIntyre
    0

    How can I unit test a class inheriting from SurfaceController?

    I have many Controller classes that inherit from the SurfaceController and I want ot be able to unit test them. Unfortunately, the Umbraco.Web.UmbracoContext has a private internal constructor, as does the Umbraco.Core.ApplicationContext. The UmbracoWeb.Routing.IRoutesCache interface itself is marked private internal so cannot be used at all, even for a stub.

    I can invoke some objects through reflection, but obviously this is less than ideal should the source change.

    What are people currently doing to overcome this? Are people simply not unit testing their SurfaceControllers?

  • Kieron McIntyre 79 posts 293 karma points
    Jan 03, 2013 @ 10:38
    Kieron McIntyre
    0

    To clarify I am using v4.10 and v4.11.

  • Luke Goodrich 9 posts 29 karma points
    Jan 29, 2013 @ 17:24
    Luke Goodrich
    0

    I would very much like to unit test SurfaceController implementations but arrived at the same conclusion.

    I've walked through the Umbraco unit tests and they do create an UmbracoContext for testing but these involve accessing a bunch of internal classes/interfaces/properties. Key part of code I looked at is GetUmbracoContext in here http://umbraco.codeplex.com/SourceControl/changeset/view/fcc62280c04b#src/Umbraco.Tests/TestHelpers/BaseWebTest.cs

    I'm not sure it is possible to achieve an elegant solution due to the fact the internal interfaces involved, but I have managed to get simple tests to run using reflection to create an UmbracoContext.

    I am also using 4.11.

     

  • shanem 39 posts 91 karma points
    Jul 16, 2013 @ 19:28
    shanem
    0

    Did you ever solve this problem? Could you post some code on how you used reflection to accomplish a simple test? Thanks!

  • Kieron McIntyre 79 posts 293 karma points
    Jul 16, 2013 @ 19:29
    Kieron McIntyre
    0

    No, I'm afraid not but to be honest I haven't tried recently. In 6.x there is a slightly different API so it may be possible .. I'll have a look and report back.

  • Lars-Erik Aabech 310 posts 1005 karma points mvp c-trib
    Jul 18, 2013 @ 08:12
    Lars-Erik Aabech
    1

    It's not a solution to your question, but what I usually do is put non-dependent code in standalone classes which I unit test.
    Then I just decorate or delegate to the class(es) with the controller without unit-testing the controller.
    It also makes it easier to use the same logic in ApiControllers for instance.

    For instance:

    public class UpdatePersonCommand
    {
    public Guid Id {get;set;}
    public string Name {get;set;}
    }

    // Unit test this
    public class UpdatePersonCommandHandler
    {
    public UpdatePersonCommandHandler(..dependencies..){}

    public void Execute(UpdatePersonCommand cmd)
    {
    // whatever logic to test
    }
    }

    public class DependentController : SurfaceController
    {
    public ActionResult UpdatePerson(UpdatePersonCommand cmd)
    {

    // use a factory or DI
    new UpdatePersonCommandHandler().Execute(cmd);
    return View(someModel);
    }
    }

    No reason to unit test implementation specifics of that controller now. ;) 
    My first MVC project was a project I had written 85% of using the MVP pattern with extensive tests of the presenters.
    However, I'd also used CQRS behind the presenters, and all of that was tested.
    Then I migrated to MVC (rather scared and reluctant), and it turned out I could throw away thousands of lines of presenter code and tests, and I ended up with the tested CQRS stuff and no controller method longer than 3 lines. (So now I love MVC :) )

    If you really need to unit-test stuff that is Umbraco specific and needs a web context, I suggest you use the adapter pattern for whatever you need and unit-test your code with fakes. It's pretty hard to make mistakes if you use Resharpers code generation feature for delegation for instance.

  • Luke Goodrich 9 posts 29 karma points
    Jul 23, 2013 @ 13:09
    Luke Goodrich
    0

    Sorry for the delay in answering. I have been away on holiday.

    The reflection code I used is below.

    Thoughts to consider;

    • This uses the Moq mocking library
    • It does not enable all Umbraco functions - the context is initialized just enough to be able to create the controller.
    • I agree with above sentiment about keeping logic out of controllers. However unit testing of controllers is still useful in some scenarios.
     using System;
        using System.Collections.Generic;
        using System.Linq;
        using System.Reflection;
        using System.Text;
        using System.Web;
        using Moq;
        using Umbraco.Core;
        using Umbraco.Web;
    
        /// <summary>
        /// Helper class which can be used to create an <see cref="UmbracoContext"/> instance for unit testing of surface controllers.
        /// </summary>
        public static class UmbracoContextHelper
        {
            #region Methods
    
            /// <summary>
            /// Helper method which creates an <see cref="UmbracoContext"/> instance populated with a <see cref="HttpContextBase"/> instance created using
            /// the specified <see cref="MockRepository"/> instance.
            /// </summary>
            /// <param name="mockRepository">The mock repository to use to create the <see cref="HttpContext"/> for the created <see cref="UmbracoContext"/></param>
            /// <param name="requestUri">The destination URL of the request being mocked.</param>
            /// <returns>An <see cref="UmbracoContext"/> instance which can be used for basic unit testing of surface controllers.</returns>
            [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design""CA1011", Justification = "Do not derive from base class as suggested because it is obselete")]
            public static UmbracoContext CreateContext(MockRepository mockRepository, Uri requestUri)
            {
                Mock<HttpContextBase> mockHttpContext = mockRepository.Create<HttpContextBase>();
                Mock<HttpRequestBase> mockHttpRequest = mockRepository.Create<HttpRequestBase>();
    
                mockHttpContext.SetupGet(p => p.Request).Returns(mockHttpRequest.Object);
                mockHttpRequest.SetupGet(p => p.Url).Returns(requestUri);
    
                return CreateContext(mockHttpContext.Object);
            }
    
            /// <summary>
            /// Helper method which sets the UmbracoContext property of the 
            /// </summary>
            /// <param name="httpContext">The <see cref="HttpContextBase"/> instance which the <see cref="UmbracoContext"/> instance wraps. The URL property must be set.</param>
            /// <returns>An <see cref="UmbracoContext"/> instance which can be used for basic unit testing of surface controllers.</returns>
            public static UmbracoContext CreateContext(HttpContextBase httpContext)
            {
                Type umbracoContextType = typeof(UmbracoContext);
                Assembly umbracoWebAssembly = umbracoContextType.Assembly;
                ApplicationContext applicationContext = CreateApplicationContext();
                BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
                Type[] umbracoContextConstructorArgTypes = new Type[] 
                { 
                    typeof(HttpContextBase),
                    typeof(ApplicationContext),
                    umbracoWebAssembly.GetType("Umbraco.Web.Routing.IRoutesCache")
                };
                ConstructorInfo umbracoContextConstructor = umbracoContextType.GetConstructor(bindingFlags, nullCallingConventions.Any, umbracoContextConstructorArgTypes, null);
                UmbracoContext umbracoContext = (UmbracoContext)umbracoContextConstructor.Invoke(new object[] { httpContext, applicationContext, null });
    
                SetRoutingContext(umbracoContext);
    
                return umbracoContext;
            }
    
            /// <summary>
            /// Helper method which sets the umbraco page ID.
            /// </summary>
            /// <param name="context">The <see cref="UmbracoContext"/> instance to set the pageid for</param>
            /// <param name="pageId">The pageId to assign to the Umbraco context.</param>
            public static void SetPageId(this UmbracoContext context, int? pageId)
            {
                HttpContextBase httpContext = context.HttpContext;
    
                httpContext.Items["pageID"] = pageId.ToString();
            }
    
            /// <summary>
            /// Creates an Umbraco.Web.Routing.RoutingContext" instance and assigns it to the 
            /// RoutingContext property of the UmbracoContext instance specified by umbracoContext.
            /// </summary>
            /// <param name="umbracoContext">The UmbracoContext instance to set the RoutingContext on.</param>
            private static void SetRoutingContext(UmbracoContext umbracoContext)
            {
                Type umbracoContextType = typeof(UmbracoContext);
                Assembly umbracoWebAssembly = umbracoContextType.Assembly;
                Type routingContextType = umbracoWebAssembly.GetType("Umbraco.Web.Routing.RoutingContext");
                BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
                Type[] routingContextConstructorArgTypes = new Type[] 
                { 
                    typeof(UmbracoContext),
                    typeof(IEnumerable<>).MakeGenericType(umbracoWebAssembly.GetType("Umbraco.Web.Routing.IPublishedContentLookup")),
                    umbracoWebAssembly.GetType("Umbraco.Web.Routing.IDocumentLastChanceLookup"),
                    umbracoWebAssembly.GetType("Umbraco.Web.IPublishedContentStore"),
                    umbracoWebAssembly.GetType("Umbraco.Web.Routing.NiceUrlProvider")
                };
                ConstructorInfo routingContextConstructor = routingContextType.GetConstructor(bindingFlags, nullCallingConventions.Any, routingContextConstructorArgTypes, null);
                PropertyInfo setContextProperty = umbracoContextType.GetProperty("RoutingContext", bindingFlags);
                object routingContext = routingContextConstructor.Invoke(new object[] { umbracoContext, nullnullnullnull });
    
                setContextProperty.SetValue(umbracoContext, routingContext, new object[] { });
            }
    
            /// <summary>
            /// Helper method which creates an ApplicationContext with very little setup.
            /// </summary>
            /// <returns>An ApplicationContext instance setup for unit testing.</returns>
            /// <remarks>
            /// <para>This should be the equivalent of BaseWebTest.Initialize() in the Umbraco codebase.</para>
            /// <code>
            /// ApplicationContext = new ApplicationContext() { IsReady = true };
            /// </code>
            /// </remarks>
            private static ApplicationContext CreateApplicationContext()
            {
                Type applicationContextType = typeof(ApplicationContext);
                BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
                ConstructorInfo constructor = applicationContextType.GetConstructor(bindingFlags, nullCallingConventions.Any, Type.EmptyTypes, null);
                PropertyInfo propertyInfo = applicationContextType.GetProperty("IsReady", bindingFlags);
                ApplicationContext context = (ApplicationContext)constructor.Invoke(null);
    
                propertyInfo.SetValue(context, truenull);
    
                return context;
            }
    
            #endregion
        }
  • Charles Afford 1163 posts 1709 karma points
    Jul 23, 2013 @ 21:04
    Charles Afford
    0

    Goodrich, will that static method do i have a mocked umbraco context that i can test against?  Thanks so much if this is true :)

  • Luke Goodrich 9 posts 29 karma points
    Jul 23, 2013 @ 23:18
    Luke Goodrich
    0

    The answer is you could. The following works with Umbraco 4.11 & 6.0.0 but not 6.1.2 as the constructor for UmbracoContext has changed.

    Which in a nutshell demonstrates Kieron's point in the original post about why this is less than ideal.

            public class DemoSurfaceController : SurfaceController
        {
            public DemoSurfaceController(UmbracoContext umbracoContext)
                : base(umbracoContext)
            {
            }
     
            public PartialViewResult SomeAction()
            {
                return this.PartialView("SomeView");
            }
        }   
        [TestClass]
        public class DemoSurfaceControllerTests
        {
            [TestMethod]
            public void TestMethod1()
            {
                MockRepository mockRepository = new MockRepository(MockBehavior.Strict);
                UmbracoContext umbracoContext = UmbracoContextHelper.CreateContext(mockRepository, new Uri("http://somesite.com/home.aspx"));
     
                DemoSurfaceController controller = new DemoSurfaceController(umbracoContext);
                PartialViewResult actionResult = controller.SomeAction();
     
                Assert.AreEqual("SomeView", actionResult.ViewName);           
            }
        }
  • Luke Goodrich 9 posts 29 karma points
    Jul 23, 2013 @ 23:29
    Luke Goodrich
    0

    The real answer here is to stop the requirement for reflection. I guess this looks something like;

    1. Make constructor for UmbracoContext public 
    2. Make the interface IPublishedCaches public so it can be mocked
    3. Add public interfaces to ContextualPublishedContentCache and ContextualPublishedMediaCache so that they can be mocked.

    I understood from some brief conversations at Codegarden that some of the code around the caching is internal because it is a moving target at the moment.

  • Luke Goodrich 9 posts 29 karma points
    Jul 28, 2013 @ 23:30
    Luke Goodrich
    0

    Right now I don't have a better way of doing this so I updated the reflection code to work with 6.1.3 - see https://github.com/lukegoodrich/SurfaceControllerTestPOC.

    To summarise previous points;

    If anybody has a better way of doing this I would really love to hear from them.

    I've also thought a bit more about what a good solution might look like. Rather than the changes in my previous post I'm wondering whether a better answer is to expose the test setups that are used by core for testing available as a unit test helper library. The code for doing the setups is in the core codebase for testing of core functionality so I imagine this would be possible. 

  • Jorge Lusar 150 posts 488 karma points
    Sep 18, 2013 @ 11:56
    Jorge Lusar
    0

    To unit test a Umbraco RenderMvcController, you need to grab the source code from github, compile the solution yourself, and get the Umbraco.Tests.dll and reference it on your test project.

    In addition to that, you need to reference the SQLCE4Umbraco.dll which is distributed with the Umbraco packages, and Rhino.Mocks.dll which is internally for mocking.

    To help you with this, I have compiled put the Umbraco.Tests.dll for Umbraco 6.1.5 and put it together with the Rhino.Mocks.dll and put it on this zip file.

    Finally, derive your test from BaseRoutingTest, override the DatabaseTestBehavior to NoDatabasePerFixture, and get the UmbracoContext and HttpBaseContext by calling the GetRoutingContext method, as in the code below:

    using System;
    using Moq;
    using NUnit.Framework;
    using System.Globalization;
    using System.Web.Mvc;
    using System.Web.Routing;
    using Umbraco.Core.Models;
    using Umbraco.Tests.TestHelpers;
    using Umbraco.Web;
    using Umbraco.Web.Models;
    using Umbraco.Web.Mvc;
    
    namespace UnitTests.Controllers
    {
        public class Entry
        {
            public int Id { get; set; }
            public string Url { get; set; }
            public string Title { get; set; }
            public string Summary { get; set; }
            public string Content { get; set; }
            public string Author { get; set; }
            public string[] Tags { get; set; }
            public DateTime Date { get; set; }
        }
    
        public interface IBlogService
        {
            Entry GetBlogEntry(int id);
        }
    
        public class BlogEntryController : RenderMvcController
        {
            private readonly IBlogService _blogService;
    
            public BlogEntryController(IBlogService blogService, UmbracoContext ctx)
                : base(ctx)
            {
                _blogService = blogService;
            }
    
            public BlogEntryController(IBlogService blogService)
                : this(blogService, UmbracoContext.Current)
            {
            }
    
            public override ActionResult Index(RenderModel model)
            {
                var entry = _blogService.GetBlogEntry(model.Content.Id);
    
                // Test will fail if we return CurrentTemplate(model) as is expecting 
                // the action from ControllerContext.RouteData.Values["action"]
                return View("BlogEntry", entry);
            }
        }
    
        [TestFixture]
        public class RenderMvcControllerTests : BaseRoutingTest
        {
            protected override DatabaseBehavior DatabaseTestBehavior
            {
                get { return DatabaseBehavior.NoDatabasePerFixture; }
            }
    
            [Test]
            public void CanGetIndex()
            {
                const int id = 1234;
                var content = new Mock<IPublishedContent>();
                content.Setup(c => c.Id).Returns(id);
                var model = new RenderModel(content.Object, CultureInfo.InvariantCulture);
                var blogService = new Mock<IBlogService>();
                var entry = new Entry { Id = id };
                blogService.Setup(s => s.GetBlogEntry(id)).Returns(entry);
                var controller = GetBlogEntryController(blogService.Object);
    
                var result = (ViewResult)controller.Index(model);
    
                blogService.Verify(s => s.GetBlogEntry(id), Times.Once());
                Assert.IsNotNull(result);
                Assert.IsAssignableFrom<Entry>(result.Model);
            }
    
            private BlogEntryController GetBlogEntryController(IBlogService blogService)
            {
                var routingContext = GetRoutingContext("/test");
                var umbracoContext = routingContext.UmbracoContext;
                var contextBase = umbracoContext.HttpContext;
                var controller = new BlogEntryController(blogService, umbracoContext);
                controller.ControllerContext = new ControllerContext(contextBase, new RouteData(), controller);
                controller.Url = new UrlHelper(new RequestContext(contextBase, new RouteData()), new RouteCollection());
                return controller;
            }
        }
    }
    

    This code has only been tested in Umbraco 6.1.5.

  • Aaron Powell 1706 posts 3039 karma points core c-trib
    Oct 01, 2013 @ 03:49
    Aaron Powell
    0

    Wow, this is like 100 times harder than it should be.

    I'm thinking I'll extract an interface on UmbracoContext and make that the return from UmbracoContext.Current and the argument that the PluginController requires be the interface not the class. This way it'll be easier to create a unit test because you can mock the interface rather than having to either use crazy reflection (which doesn't work in 6.1 as the types are different) or taking a database dependency as Jorge has done.

  • Kieron McIntyre 79 posts 293 karma points
    Oct 01, 2013 @ 09:45
    Kieron McIntyre
    0

    Hi Slace, that would be awesome. You're right it is a lot harder than it needs to be, especially when so much work has already been done to loosely couple things in the CMS.

  • William Charlton 160 posts 185 karma points
    Oct 08, 2013 @ 19:09
    William Charlton
    0

    Hmmmmmmmmmm Umbraco. I think the previous poster summed it up - Umbraco is a lot harder than it needs to be. One almost gets the impreession it is by design - unless you live, sleep and breath .net code you ain't going to run Umbraco - forget it. It is full of undocumented bugs, it's flakily designed, it's over complex and relies on cutting edge services when it probably doesn't need to.

    I am trying to convert an existing 4.11.1 site to use 6.1.3.

    6.1.4, 6.1.5 and 6.1.6 have catastrophic bugs that don't allow me to save a Template so they are non-starters.

    I got the site built - finally - and decide to use Contour for our forms - previously we built our own, a feat in itself.

    So (being prudent) I killed SQL2008, zipped up the site and attempted to install Contour (stupidly I tried the Manual install UmbracoContour_3.0.17_Manual_Install.zip until I realised that "manual" meant running the SQL scripts and farting about with all the files needed. I then tried the one I should have tried UmbracoContour_3.0.17.zip.

    All went well until the end where I got 2 errors - unable to write to \config\dashboard.config and unable to update \config\lang\en.xml. So I fixed the permissions and went for an uninstall - wrong!!!

    First, I was logged out and cold not log back in. On closing and firing up the site I got an error due to a line that Contour added to the web.config, no problem, I deleted the line and tried again.

    I go a whole load of errors so decided to roll back to the previous version, deleted the web root and restored the zipped files - wrong again!!!

    I then got an error - unable to access \App_Data\TEMP\PluginCache\umbraco-plugins.MACHINENAME.hash and \App_Data\TEMP\PluginCache\umbraco-plugins.MACHINENAME.hash

    So I allowed Modify rights to both of those. I now get:

    Current has not been initialized on Umbraco.Web.PublishedCache.PublishedCachesResolver. You must initialize Current before trying to read it.

    WTF?

    Tomorrow I will start all over again.

    It should not have to be this way.

  • Kieron McIntyre 79 posts 293 karma points
    Oct 08, 2013 @ 19:37
    Kieron McIntyre
    0

    Hi William, I'm sorry you've had these problems but you seem to have vented your frustration in a thread discussing unit testing.

  • William Charlton 160 posts 185 karma points
    Oct 09, 2013 @ 13:36
    William Charlton
    0

    Kieron,

    I have vented my frustration in several places. I was aware that this thread was not 100% relevant to my problems but the basic issues of the direction of travel needs to be addressed and unit testers are as good an audience as any. There are some basic issues that need addressing. The barriers for entry are high already and seem to be getting higher. It has been acknowledged that Umbarco is complex because it is flexible. OK, I buy that but it COULD be simpler if some thought was given to the structure. It was Slace's comment "Wow, this is like 100 times harder than it should be." that hit a nerve. If you guys are struggling, what hope has the average .Net dev, apart from the doctype/datatype/template complexity inherent in the Umbraco structure. Unit testers play a  part it keeping it simple - and therefore stable. As you rightly said "it is a lot harder than it needs to be" - at all levels, that's if it works at all.

    Thanks for the reply though, it is one of the most helpful I've had yet.

  • Dipun 4 posts 25 karma points
    Nov 09, 2013 @ 03:23
    Dipun
    0

    Hey guys,

    I just wanted to let you know that I have done some deep investigation in the umbraco source and I have documented a solution to using mvc, umbraco and keeping things unit testable. It is quite a long read, but it basically allows you to use mvc controllers instead of surface controllers and suggests reasonable ways to overcome the lack of surface controller features.

    http://dipunm.wordpress.com/2013/08/26/my-solution-for-a-fully-testable-umbraco-project/

     

Please Sign in or register to post replies

Write your reply to:

Draft