Before we put the UI through its paces, let's look at the TDD aspects of our port.
Getting Unit tests working
From a TDD perspective, with NUnit & NMock, we cannot escape the conclusion that major parts of the source need to be re-written. In particular, the 4.7.2 source code contains a long static chain - a chain of objects with static variables & functions, that render testing with NUnit & NMock impossible. In general, many testing frameworks do not allow for mocking of static chain elements, and this style of coding has consequently come into dis-favor.
It would be preferable for example to have a very small static application loader class, that instantiates all relevant variables & constants, and initializes data services. In .NET, this could be done in the global.asax file. Then for testing, we isolate all database read / write calls, which are tested separately. The functions that use database services are tested separately using a test data service and mocks. This has the benefit of testing application functionality and database access in isolation of each other and is especially helpful for diagnosing any multi-platform issues.
Of course, a major re-write of 4.7.2 is beyond the scope of this porting exercise - which is all about porting an existing application with minimal changes. But these issues can be addressed by the next 4.x version.
In this section, I will show what minimum changes are needed to get unit tests working in Linux. The changes rely on some clever minor re-writing of the existing code to inject test values through dependency injection. It is not the prettiest approach, but it works.
Monodevelop does not recognize the Umbraco.Test project
Open the Umbraco.Test.csproj file and delete the {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} line
The Umbraco.Test project will not load in the solution.
Now in the Solution Options > Build Configurations, Configuration Mappings, add Umbraco.Test to the build.
Setting up Nunit tests
Download the NUnit dlls. In the project references, delete the 'Microsoft.VisualStudio.QualityTools.UnitTestFramework, and add nunit.framework.dll
Test Conversion
Using search replace tools, for every test replace 'using Microsoft.VisualStudio.TestTools.UnitTesting;' with 'using NUnit.Framework;'
Using this link convert all test calls.
The tests now compile.
Getting Tests to Pass
All tests run but fail. I have started fixing these tests. Fix 1: establish database access. NUnit does not have access to the .NET Application environment, and calls to Configuration.Appsettings[key] return null. This causes tests requring database access to fail.
Solution: inject database settings.
Code re-writes for injecting test Appsettings
I target the static umbraco/businesslogic/GlobalSettings.cs class DbDSN property (line 139), and replace the static system call with a lazy loading singleton that mimics static behavior:
//return ConfigurationManager.AppSettings["umbracoDbDSN"];
return ConfigurationManagerService.Instance.AppSettings["umbracoDbDSN"];
The ConfigurationManagerService class creates and returns an instance of the default or a test configurator.
In tests we can set the test configurator with: ConfigurationManagerService.ConfigManager = new ConfigurationManagerTest(SetUpUtilities.GetAppSettings());
If the 'ConfigManager' property is null, then the service defaults to the runtime, non-test configurator.
All of this is probably not making much sense, and seeing the whole code will help.
1. First we create an interface for the ConfigurationManager calls that we are using.:
// .../umbraco/interfaces/IConfigurationManager.cs
using System;
using System.Collections.Specialized;
using System.Web;
namespace umbraco.interfaces
{
public interface IConfigurationManager
{
NameValueCollection AppSettings {get;}
Object GetSection(string SectionName);
void RefreshSection(string SectionName);
}
}
2. Next we create our wrappers (aka Facade pattern)
// .../umbraco/businesslogic/ConfigurationManager/ConfigurationManagerDefault.cs
using System;
using System.Collections.Specialized;
using System.Configuration;
using System.Web;
using umbraco.interfaces;
namespace umbraco.BusinessLogic
{
public class ConfigurationManagerDefault : IConfigurationManager
{
public ConfigurationManagerDefault (){}
public NameValueCollection AppSettings
{
get
{
return ConfigurationManager.AppSettings;
}
}
public Object GetSection(string SectionName)
{
return ConfigurationManager.GetSection(SectionName);
}
public void RefreshSection(string SectionName)
{
ConfigurationManager.RefreshSection(SectionName);
}
}
}
// .../umbraco/businesslogic/ConfigurationManager/ConfigurationManagerTest.cs
using System;
using System.Collections.Specialized;
using System.Configuration;
using System.Web;
using umbraco.interfaces;
namespace umbraco.BusinessLogic
{
public class ConfigurationManagerTest : IConfigurationManager
{
public ConfigurationManagerTest(NameValueCollection appSettings)
{
_appSettings = new NameValueCollection();
_appSettings.Add(appSettings);
}
private NameValueCollection _appSettings;
public NameValueCollection AppSettings
{
get
{
return MergeAppSettings(ConfigurationManager.AppSettings);
}
}
public Object GetSection(string SectionName)
{
return ConfigurationManager.GetSection(SectionName);
}
public void RefreshSection(string SectionName)
{
ConfigurationManager.RefreshSection(SectionName);
}
private NameValueCollection MergeAppSettings(NameValueCollection appSettings)
{
NameValueCollection mergedAppSettings;
mergedAppSettings = new NameValueCollection();
if (appSettings.HasKeys())
foreach (string key in appSettings)
{
if (_appSettings[key] != null)
mergedAppSettings.Add(key.ToString(), _appSettings[key].ToString());
else
mergedAppSettings.Add(key.ToString(), appSettings[key].ToString());
}
else
mergedAppSettings.Add(_appSettings);
return mergedAppSettings;
}
}
}
3. We create the injector
// .../umbraco/businesslogic/ConfigurationManager/ConfigurationManagerFactory.cs
using System;
using System.Collections.Specialized;
using System.Configuration;
using System.Web;
using umbraco.interfaces;
namespace umbraco.BusinessLogic
{
public class ConfigurationManagerFactory
{
private ConfigurationManagerFactory() {}
public static IConfigurationManager GetConfigManager(IConfigurationManager configManager)
{
if (configManager == null)
return new ConfigurationManagerDefault();
else
return configManager;
}
}
}
4. We create the singleton
// .../umbraco/businesslogic/ConfigurationManager/ConfigurationManagerService.cs
using System;
using System.Collections.Specialized;
using System.Configuration;
using System.Web;
using umbraco.interfaces;
namespace umbraco.BusinessLogic
{
public sealed class ConfigurationManagerService
{
private static volatile IConfigurationManager _instance;
private static object _syncRoot = new Object();
private static IConfigurationManager _configManager = null;
public static IConfigurationManager ConfigManager
{
get {return _configManager;}
set
{
_configManager = value;
}
}
private ConfigurationManagerService (){}
public static IConfigurationManager Instance
{
get
{
if (_instance == null)
{
lock(_syncRoot)
{
if (_instance == null)
_instance = ConfigurationManagerFactory.GetConfigManager(_configManager);
}
}
return _instance;
}
}
}
}
5. So far, we have not modified any existing source code but now we do that.
// .../umbraco/businesslogic/GlobalSettings.cs line 139
public static string DbDSN
{
get
{
try
{
//return ConfigurationManager.AppSettings["umbracoDbDSN"];
return ConfigurationManagerService.Instance.AppSettings["umbracoDbDSN"];
}
// ...
6. We can use the following quick and dirty test setup (it would be better to read the settings from a file).
// .../umbraco.Test/SetUpUtilities.cs
using System;
using System.Collections.Specialized;
namespace umbraco.Test
{
public class SetUpUtilities
{
public SetUpUtilities () {}
private const string _umbracoDbDSN = "server=127.0.0.1;database=someDB;user id=someUser;password=somePwd;datalayer=MySql";
public static NameValueCollection GetAppSettings()
{
NameValueCollection appSettings = new NameValueCollection();
//add application settings
appSettings.Add("umbracoDbDSN", _umbracoDbDSN);
return appSettings;
}
}
}
7. A simple test example
// .../umbraco.Test/ApplicationTest.cs
using NUnit.Framework;
using System;
using umbraco.interfaces;
using System.Collections.Generic;
using umbraco.DataLayer;
using System.Linq;
namespace umbraco.Test
{
///
///This is a test class for ApplicationTest and is intended
///to contain all ApplicationTest Unit Tests
///
[TestFixture]
public class ApplicationTest
{
[TestFixtureSetUp]
public void InitTestFixture()
{
ConfigurationManagerService.ConfigManager = new ConfigurationManagerTest(SetUpUtilities.GetAppSettings());
}
[Test]
public void Application_Make_New()
{
var name = Guid.NewGuid().ToString("N");
Application.MakeNew(name, name, "icon.jpg");
//check if it exists
var app = Application.getByAlias(name);
Assert.IsNotNull(app);
//now remove it
app.Delete();
Assert.IsNull(Application.getByAlias(name));
}
}
}