Created
February 27, 2018 21:11
-
-
Save AndyButland/7c213cc0bd31116ffdb18d12566b9361 to your computer and use it in GitHub Desktop.
MSTest based Fitness Function for Verification of Sitecore Helix Architecture
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace MySolution.FitnessFunctions | |
{ | |
using System; | |
using System.Collections.Generic; | |
using System.Diagnostics; | |
using System.IO; | |
using System.Linq; | |
using System.Text.RegularExpressions; | |
using System.Xml; | |
using FluentAssertions; | |
using Microsoft.VisualStudio.TestTools.UnitTesting; | |
/// <summary> | |
/// Fitness functions for software architecture are defined as a way of confirming that a particular | |
/// characteristic of a solution's architecture that is considered important to maintain over time, | |
/// is in fact still the case. | |
/// Some of these can be confirmed via automated tests. | |
/// This class defines tests that ensure the project reference structure as defined by Sitecore | |
/// Helix principles are being maintained. | |
/// </summary> | |
/// <remarks> | |
/// References: | |
/// https://www.thoughtworks.com/insights/blog/microservices-evolutionary-architecture | |
/// http://shop.oreilly.com/product/0636920080237.do | |
/// http://helix.sitecore.net/introduction/index.html | |
/// </remarks> | |
[TestClass] | |
public class HelixFitnessFunctionTests | |
{ | |
[TestClass] | |
public class TheSolutionShould : HelixFitnessFunctionTests | |
{ | |
protected static readonly string SolutionName = "MySolution.sln"; | |
protected static readonly string Foundation = "Foundation"; | |
protected static readonly string Feature = "Feature"; | |
protected static readonly string Project = "Project"; | |
protected static readonly string TestProjectSuffix = ".Tests"; | |
/// <summary> | |
/// Ensures that only appropriate inter-project references are in place within the solution. | |
/// Specifically: | |
/// - Foundation projects should only reference other Foundation projects | |
/// - Feature projects should only reference Foundation projects | |
/// - Project projects should only reference Foundation or Feature projects | |
/// </summary> | |
/// <remarks> | |
/// Hat-tip for locating projects and references in solution: https://stackoverflow.com/a/17571223/489433 | |
/// </remarks> | |
[TestMethod] | |
public void ContainNoHelixInvalidProjectReferences() | |
{ | |
// Arrange - get list of projects in the solution, along with a list of references for each project | |
Dictionary<string, IList<string>> projects = GetProjectsWithReferences(); | |
// Act - review project list for invalid references | |
var invalidFoundationReferences = new List<string>(); | |
var invalidFeatureReferences = new List<string>(); | |
var invalidProjectReferences = new List<string>(); | |
foreach (var project in projects) | |
{ | |
if (project.Key.Contains(Foundation)) | |
{ | |
GetInvalidReferences(project, new[] { Feature, Project }, invalidFoundationReferences); | |
} | |
else if (project.Key.Contains(Feature)) | |
{ | |
GetInvalidReferences(project, new[] { Feature, Project }, invalidFeatureReferences); | |
} | |
else if (project.Key.Contains(Project)) | |
{ | |
GetInvalidReferences(project, new[] { Project }, invalidProjectReferences); | |
} | |
} | |
// Assert - check if we have any invalid refererences | |
AssertLayerReferences(invalidFoundationReferences, Foundation); | |
AssertLayerReferences(invalidFeatureReferences, Feature); | |
AssertLayerReferences(invalidProjectReferences, Project); | |
} | |
private static Dictionary<string, IList<string>> GetProjectsWithReferences() | |
{ | |
var solutionFilePath = GetSolutionFilePath(); | |
var solutionFileContents = File.ReadAllText(solutionFilePath); | |
var matches = GetProjectsFromSolutionFileContents(solutionFileContents); | |
return matches | |
.Select(x => x.Groups[2].Value) | |
.ToDictionary(GetProjectFileName, p => GetReferencedProjects(solutionFilePath, p)); | |
} | |
private static string GetSolutionFilePath() | |
{ | |
var pathParts = GetPathParts(Directory.GetCurrentDirectory()); | |
var rootIndex = pathParts.IndexOf("src"); | |
return Path.Combine(string.Join("\\", pathParts.Take(rootIndex)), SolutionName); | |
} | |
private static IList<string> GetPathParts(string directory) | |
{ | |
return directory | |
.Split(new[] { "\\" }, StringSplitOptions.None) | |
.ToList(); | |
} | |
private static IEnumerable<Match> GetProjectsFromSolutionFileContents(string solutionFileContents) | |
{ | |
var regex = new Regex( | |
"Project\\(\"\\{[\\w-]*\\}\"\\) = \"([\\w _]*.*)\", \"(.*\\.(cs|vcx|vb)proj)\"", | |
RegexOptions.Compiled); | |
return regex.Matches(solutionFileContents).Cast<Match>(); | |
} | |
private static string GetProjectFileName(string value) | |
{ | |
return GetPathParts(value).Last().Replace(".csproj", string.Empty); | |
} | |
private static IList<string> GetReferencedProjects(string solutionFilePath, string projectFilePath) | |
{ | |
var rootedProjectFilePath = Path.Combine(solutionFilePath.Replace(SolutionName, string.Empty), projectFilePath); | |
return GetReferencedProjects(rootedProjectFilePath); | |
} | |
private static List<string> GetReferencedProjects(string rootedProjectFilePath) | |
{ | |
var result = new List<string>(); | |
var xmlDoc = new XmlDocument(); | |
xmlDoc.Load(rootedProjectFilePath); | |
var ns = new XmlNamespaceManager(xmlDoc.NameTable); | |
ns.AddNamespace("tu", "http://schemas.microsoft.com/developer/msbuild/2003"); | |
var rootNode = xmlDoc.DocumentElement; | |
var referenceNodes = rootNode?.SelectNodes("//tu:ProjectReference", ns); | |
if (referenceNodes == null) | |
{ | |
return result; | |
} | |
foreach (XmlNode node in referenceNodes) | |
{ | |
result.Add(node.ChildNodes[1].InnerText); | |
} | |
return result; | |
} | |
private static void GetInvalidReferences(KeyValuePair<string, IList<string>> project, string[] invalidLayers, List<string> invalidReferences) | |
{ | |
invalidReferences.AddRange(project.Value | |
.Where(x => ContainsInvalidReference(project.Key, x, invalidLayers)) | |
.Select(x => $"{project.Key} references {x}")); | |
} | |
private static bool ContainsInvalidReference(string projectName, string referencedProjectName, IEnumerable<string> invalidLayers) | |
{ | |
return invalidLayers | |
.Any(x => ProjectReferencesInvalidLayer(referencedProjectName, x) && | |
!ReferencedProjectIsFromDedicatedTestProject(projectName, referencedProjectName) && | |
!ReferencedProjectIsPartOfSubFeature(projectName, referencedProjectName)); | |
} | |
private static bool ProjectReferencesInvalidLayer(string referencedProject, string invalidLayer) | |
{ | |
return referencedProject.Contains(invalidLayer); | |
} | |
private static bool ReferencedProjectIsFromDedicatedTestProject(string projectName, string referencedProject) | |
{ | |
return projectName == referencedProject + TestProjectSuffix; | |
} | |
private static bool ReferencedProjectIsPartOfSubFeature(string projectName, string referencedProject) | |
{ | |
// We have some features that are broken into more than one project. Via naming convention | |
// we should allow these to be valid. | |
// E.g. MySolution.Feature.FeatureName.Api references MySolution.Feature.FeatureName.Model | |
if (projectName.Split('.').Length <= 3 || referencedProject.Split('.').Length <= 3) | |
{ | |
// Not a sub-feature | |
return false; | |
} | |
return projectName.Split('.')[2] == referencedProject.Split('.')[2]; | |
} | |
private static void AssertLayerReferences(IReadOnlyCollection<string> invalidReferences, string layer) | |
{ | |
invalidReferences.Count.Should().Be(0, "there should be no Helix incompatible references in the {1} layer. {0} invalid reference{2} found: {3}. Expected 0", | |
invalidReferences.Count, | |
layer, | |
invalidReferences.Count == 1 ? " was" : "s were", | |
string.Join(", ", invalidReferences)); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment