From 2dcf00e4822cf0712e29b7ba76fd8599fc8eb3f6 Mon Sep 17 00:00:00 2001 From: Ivan Pozdnyakov Date: Fri, 24 Jul 2015 18:12:35 -0400 Subject: [PATCH] first commit --- BasicApp.App/BasicApp.App.csproj | 225 +++++++++++++ BasicApp.App/Program.cs | 22 ++ BasicApp.App/Properties/AssemblyInfo.cs | 36 +++ BasicApp.App/Utilities.cs | 386 +++++++++++++++++++++++ BasicApp.App/packages.config | 6 + BasicApp.Test/BasicApp.Test.csproj | 76 +++++ BasicApp.Test/Properties/AssemblyInfo.cs | 36 +++ BasicApp.Test/UtilitiesUnderTest.cs | 341 ++++++++++++++++++++ BasicApp.Test/packages.config | 4 + 9 files changed, 1132 insertions(+) create mode 100644 BasicApp.App/BasicApp.App.csproj create mode 100644 BasicApp.App/Program.cs create mode 100644 BasicApp.App/Properties/AssemblyInfo.cs create mode 100644 BasicApp.App/Utilities.cs create mode 100644 BasicApp.App/packages.config create mode 100644 BasicApp.Test/BasicApp.Test.csproj create mode 100644 BasicApp.Test/Properties/AssemblyInfo.cs create mode 100644 BasicApp.Test/UtilitiesUnderTest.cs create mode 100644 BasicApp.Test/packages.config diff --git a/BasicApp.App/BasicApp.App.csproj b/BasicApp.App/BasicApp.App.csproj new file mode 100644 index 0000000..f8fbd44 --- /dev/null +++ b/BasicApp.App/BasicApp.App.csproj @@ -0,0 +1,225 @@ + + + + Debug + x86 + 8.0.30703 + 2.0 + {F7EBFB21-F051-4053-B3F9-D5545CC506D3} + Exe + Properties + BasicApp.App + BasicApp.App + v4.0 + Client + 512 + ..\..\ + true + + + x86 + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + x86 + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\NAnt.Extensions.0.1.2.4\lib\CollectionGen.dll + True + + + ..\..\packages\dotless.1.2.2.0\lib\dotless.Core.dll + True + + + ..\packages\NAnt.Extensions.0.1.2.4\lib\ICSharpCode.SharpZipLib.dll + True + + + ..\packages\NAnt.Extensions.0.1.2.4\lib\Interop.MsmMergeTypeLib.dll + True + True + + + ..\packages\NAnt.Extensions.0.1.2.4\lib\Interop.StarTeam.dll + True + True + + + ..\packages\NAnt.Extensions.0.1.2.4\lib\Interop.WindowsInstaller.dll + True + True + + + ..\packages\NAnt.Extensions.0.1.2.4\lib\log4net.dll + True + + + ..\packages\NAnt.Extensions.0.1.2.4\lib\NAnt.exe + .exe + True + + + ..\packages\NAnt.Extensions.0.1.2.4\lib\NAnt.CompressionTasks.dll + True + + + ..\packages\NAnt.Extensions.0.1.2.4\lib\NAnt.Contrib.Tasks.dll + True + + + ..\packages\NAnt.Extensions.0.1.2.4\lib\NAnt.Core.dll + True + + + ..\packages\NAnt.Extensions.0.1.2.4\lib\NAnt.DotNetTasks.dll + True + + + ..\packages\NAnt.Extensions.0.1.2.4\lib\NAnt.Extensions.Tasks.dll + True + + + ..\packages\NAnt.Extensions.0.1.2.4\lib\NAnt.MSNetTasks.dll + True + + + ..\packages\NAnt.Extensions.0.1.2.4\lib\NAnt.SourceControlTasks.dll + True + + + ..\packages\NAnt.Extensions.0.1.2.4\lib\NAnt.VisualCppTasks.dll + True + + + ..\packages\NAnt.Extensions.0.1.2.4\lib\NAnt.VSNetTasks.dll + True + + + ..\packages\NAnt.Extensions.0.1.2.4\lib\NAnt.Win32Tasks.dll + True + + + ..\packages\NAnt.Extensions.0.1.2.4\lib\NAntContrib.dll + True + + + ..\packages\NAnt.Extensions.0.1.2.4\lib\NDoc.Documenter.NAnt.dll + True + + + ..\packages\NAnt.Extensions.0.1.2.4\lib\scvs.exe + .exe + True + + + ..\packages\NAnt.Extensions.0.1.2.4\lib\SLiNgshoT.exe + .exe + True + + + ..\packages\NAnt.Extensions.0.1.2.4\lib\SLiNgshoT.Core.dll + True + + + ..\packages\NAnt.Extensions.0.1.2.4\lib\SourceSafe.Interop.dll + True + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/BasicApp.App/Program.cs b/BasicApp.App/Program.cs new file mode 100644 index 0000000..f25100c --- /dev/null +++ b/BasicApp.App/Program.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using BasicApp.App.Utilities; + +namespace BasicApp.Main +{ + class Program + { + static void Main(string[] args) + { + UtilitiesDate utility = new UtilitiesDate(); + utility.SetMonthsUsingWriterInput("emptyFile.txt"); + Console.WriteLine(utility.GetMonths()); + Console.WriteLine(utility.GetWeeks(DateTime.Now.AddDays(-42), DateTime.Now)); + Console.WriteLine(utility.GetDays(DateTime.Now.AddDays(-42), DateTime.Now)); + Console.WriteLine("Press enter to close..."); + Console.ReadLine(); + } + } +} diff --git a/BasicApp.App/Properties/AssemblyInfo.cs b/BasicApp.App/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..8708bff --- /dev/null +++ b/BasicApp.App/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("BasicApp.App")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("BasicApp.App")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("fa8a923b-9fb8-4092-a783-ed0220a848eb")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/BasicApp.App/Utilities.cs b/BasicApp.App/Utilities.cs new file mode 100644 index 0000000..dda7c4f --- /dev/null +++ b/BasicApp.App/Utilities.cs @@ -0,0 +1,386 @@ +using System; +using System.Text.RegularExpressions; + +namespace BasicApp.App.Utilities +{ + public class UtilitiesProfession + { + public enum Gender + { + Male = 1, + Female = 2 + } + + public string GetCompleteProfession(string professionName, Gender g) + { + string StrPronoun = string.Empty; + + if (g == Gender.Male) + StrPronoun = "he"; + else + StrPronoun = "she"; + + if (Regex.IsMatch(professionName, "^[aeiou]")) + return StrPronoun + " is an " + professionName; + else + return StrPronoun + " is a " + professionName; + } + } + + public class UtilitiesDate + { + private IReader _Reader; + private IWriter _Writer; + private string _MonthsAsString; + + private void SetMonthsAsString() + { + _MonthsAsString = ""; + _MonthsAsString += "January\r\nFebruary\r\nMarch\r\n"; + _MonthsAsString += "April\r\nMay\r\nJune\r\n"; + _MonthsAsString += "July\r\nAugust\r\nSeptember\r\n"; + _MonthsAsString += "October\r\nNovember\r\nDecember"; + } + + // Default Constructor + + public UtilitiesDate(){ + SetMonthsAsString(); + } + + // Get time difference in weeks or days from DateTime t1 to DateTime t2. + + public decimal GetWeeks(DateTime dtFrom, DateTime dtTo) + { + int Days = ((TimeSpan)(dtTo - dtFrom)).Days; + decimal Weeks = Math.Ceiling((decimal)Days / 7); + return Weeks; + } + public decimal GetDays(DateTime dtFrom, DateTime dtTo) + { + int Days = ((TimeSpan)(dtTo - dtFrom)).Days; + return Days; + } + + /*********************/ + /* STUBS: CASE STUDY */ + /*********************/ + + /* There are multiple ways by which we can inject stubs into our code, the challenge is choosing + * the correct location for the "seam", which is defined as a place where we can choose either + * a fake or the real to use in execution. + * + * The following stub injection tricks, explored in The Art of Unit Testing, are shown here and will + * be the center of this study. + * + * Injection by Parameter + * Injection by Constructor + * Injection by Property Setters + * Injection using Factory + * Injection using Local Factory (Extract & Override) + */ + + /* The following method reads a simple project text file that contains all the months listed in order. + * It will return an array of strings where each element is the name of a month. We will use it as a test + * case. + */ + public string[] GetMonths() + { + IReader Reader = new FileReader(); + string Months = Reader.ValidateAndRead("file.txt"); + return Regex.Split(Months, "\r\n"); + } + + /* INJECTION BY PARAMETER + * + * Simple & trival way to inject stub, but you might not want to expose dependency on + * reader in public method. Not much else to say. + */ + public string[] GetMonthsStubByParamInject(IReader reader) + { + string Months = reader.ValidateAndRead("file.txt"); + return Regex.Split(Months, "\r\n"); + } + + /* INJECTION BY CONSTRUCTOR + * + * Simple but won't scale well as more stubs will require more properties which means more constructors + * and method arguements in constructors. Besides being messy it also leads to potential + * time wasting. Imagine that you have 50 tests against the constructor and you find a new dependency + * that requires a stub. You'll need to add a parameter to the constructor in all those tests. + * + * A few potential solutions to constructor injection may include ... + * + * To handle neatness: a special wrapper function that locally defines values needed to initialize a class. + * And only one paramter is needed to define the class type. + * + * To handle dependancy issues: Inversion of Control (IoC). A construct where you "say" what you want and it goes "back" + * to initialize the needed dependencies to then give you what you want. More details can be found here. + * http://www.hanselman.com/blog/ListOf-NETDependencyInjectionContainersIOC.aspx. + */ + + public UtilitiesDate(IReader reader) + { + _Reader = reader; + SetMonthsAsString(); + } + + public string[] GetMonthsStubByConstructorInject() { + string Months = _Reader.ValidateAndRead("file.txt"); + return Regex.Split(Months, "\r\n"); + } + + /* INJECTION BY PROPERTY SETTERS + * + * Our method is no different than before but now we can set our reader outside of the constructor. + * We make it explicity clear that the reader is optional and keep our constructors clean and simple. + */ + + public IReader Reader + { + get { return _Reader; } + set { _Reader = value; } + } + + public string[] GetMonthsStubByPropertyInject() { + string Months = _Reader.ValidateAndRead("file.txt"); + return Regex.Split(Months, "\r\n"); + } + + /* INJECTION USING FACTORY + * + * It be cool if we could get our stub locally, right before the actual function call we are testing, instead of + * relying on global fields. To do this we need a factory. The factory class will now be responsible for generating + * our object. It will contain the seam in our code (the point at which we can generate a fake or a real). Since it's + * static we can define what ReaderFactory will output on Create. + */ + + public string[] GetMonthsStubUsingFactory() { + _Reader = ReaderFactory.Create(); + string Months = _Reader.ValidateAndRead("file.txt"); + return Regex.Split(Months, "\r\n"); + } + + /* One of the important concepts to understand within unit tests are the layers of indirection. Whenever you test you are always + * testing a certain layer of your program. Whether it be the main function or a subroutine on the extremity of the application you + * are always somewhere within the call stack. Typically the deeper you go the more control you have over what you are testing but on + * the other hand these low level tests are obscure and difficult to understand. + * + * Choosing where you want to place the seams is one of the major challenges of developing good unit tests. + * + * Thus far we have looked at two important locations where seams (specifically fakes) can be introduced. + * + * Firstly we looked at choosing to fake the members in the tested class. One of the key issues here is that we are + * changing the semantics of the code within the class we are testing, more importantly how it must be used. + * + * Secondly we looked at choosing to fake the members inside of the factory class and with that we managed to avoided changing the + * code inside the class which we are testing. + */ + + + /* INJECTION USING LOCAL FACTORY (EXTRACT AND OVERRIDE) + * + * Is an easy way to avoid depedancy issues when creating stubs by using a derivation of the class that is under test. We can + * call the derived version the "testable" class. It's incredible clean and simple, it should be your default technique for stub + * creation. The only time when you might want to use the other techniques is if the code base is already supportive of the other + * techniques. For example there's an interface available for faking (you don't need to make one) or a location in the code where a + * seam can be injected. + */ + + protected virtual IReader GetReader() + { + return new FileReader(); + } + + public string[] GetMonthsStubUsingLocalFactory() + { + _Reader = GetReader(); + string Months = _Reader.ValidateAndRead("file.txt"); + return Regex.Split(Months, "\r\n"); + } + + /* One problem with Testable Object Oriented Design (TOOD) is that it requires a lot of exposure of a class's internals. Arguably this counters + * the encapsalation principle. If you feel making extra public getters, setters and constructors for testing isn't the way you want to go + * you can always make these methods internal (over public) and use conditional attributes to make sure they are only used during + * debug builds. Having these in your code will make it less readable, primary reason I didn't include them here, but it's an option + * if security and testing are both essential. + */ + + /*********************/ + /* MOCKS: CASE STUDY */ + /*********************/ + + /* The difference between mocks and stubs can seem subtle at times. A mock is an object + * that we can use for assertion. When object A interacts with object B that should lead to a + * certain state change in B, if it doesn't the interaction failed. In this scenario we can make a + * mock of B. A more technical difference between mocks and stubs is that a correctly written stub can never + * make a test fail, but a mock can. + * + * The following Mocking techniques are explored. + * + * Use of Manually Written Mocks (Reference test code for documentation) + * Use of Strict Mocks (Reference test code for documentation) + * Use of Non-Strict Mocks (Reference test code for documentation) + * Use of Stubs (Reference test code for documentation) + * Constraints (Reference test code for documentation) + * Delegates (Reference test code for documentation) + * AAA (Arrange, Assert, Act) (Reference test code for documentation) + * + */ + + /* The following method will be used as a test case, like the previously defined getMonths, except this time we + * will be using it to test mocks. The method will simply write all the months in year to a file in the designated + * path. Each month will be written on a new line. If we successfully manage to write we return true else we return false. + * An important note here is that we aren't mocking the file system but rather the object that makes the low level + * call to write to the file system. + */ + + protected virtual IWriter GetWriter() + { + return new FileWriter(); + } + + public bool SetMonths(string path) + { + _Writer = GetWriter(); + if (_Writer.ValidateAndWrite(path, _MonthsAsString)) + return true; + else + return false; + } + + public bool SetMonthsUsingWriterInput(string path) + { + _Writer = GetWriter(); + if (_Writer.ValidateAndWrite(new WriterInput(path, _MonthsAsString))) + return true; + else + return false; + } + } + + /*MANUALLY WRITTEN STUB*/ + + public interface IReader + { + string ValidateAndRead(string path); + } + + public class FileReader : IReader + { + public string ValidateAndRead(string path) + { + return System.IO.File.ReadAllText(path); + } + } + + public class StubReader : IReader + { + public string ValidateAndRead(string path) + { + string S = ""; + S += "January\r\nFebruary\r\nMarch\r\n"; + S += "April\r\nMay\r\nJune\r\n"; + S += "July\r\nAugust\r\nSeptember\r\n"; + S += "October\r\nNovember\r\nDecember"; + return S; + } + } + + /*MANUALLY WRITTEN MOCK*/ + public interface IWriter + { + bool ValidateAndWrite(string path, string content); + bool ValidateAndWrite(WriterInput input); + } + + public class FileWriter : IWriter + { + public bool ValidateAndWrite(string path, string content) + { + try { + System.IO.File.WriteAllText(path, content); + return true; + } catch { + return false; + } + } + + public bool ValidateAndWrite(WriterInput input) { + // parse the WriterInput object and do the same as before + return true; + } + } + + public class MockWriter : IWriter + { + public string _Path; + public string _Content; + public bool ValidateAndWrite(string path, string content) + { + _Path = path; + _Content = content; + return true; + } + + public bool ValidateAndWrite(WriterInput input){ + // parse the WriterInput object and do the same as before + return true; + } + } + + /*HELPERS*/ + + public class WriterInput + { + private string _Path; + private string _Content; + + public string Path + { + get {return _Path; } + set {_Path = value;} + } + + public string Content + { + get { return _Content; } + set { _Content = value; } + } + + public WriterInput(string path, string content) { + _Path = path; + _Content = content; + } + } + + public class ReaderFactory + { + private static IReader _Reader = null; + + public static IReader Create() { + if (_Reader != null) + return _Reader; + else + return new FileReader(); + } + + public static void SetReader(IReader reader){ + _Reader = reader; + } + } + + public class TestableUtilitiesDate : UtilitiesDate { + public IReader _Reader; + public IWriter _Writer; + + protected override IReader GetReader() { + return _Reader; + } + + protected override IWriter GetWriter() + { + return _Writer; + } + } +} \ No newline at end of file diff --git a/BasicApp.App/packages.config b/BasicApp.App/packages.config new file mode 100644 index 0000000..22730fb --- /dev/null +++ b/BasicApp.App/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/BasicApp.Test/BasicApp.Test.csproj b/BasicApp.Test/BasicApp.Test.csproj new file mode 100644 index 0000000..f4b7b3d --- /dev/null +++ b/BasicApp.Test/BasicApp.Test.csproj @@ -0,0 +1,76 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {26911987-92C3-4F66-8E05-C82689C0B181} + Library + Properties + BasicApp.Test + BasicApp.Test + v4.0 + 512 + ..\..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + .exe + ..\..\..\..\builds\BasicApp\src\BasicApp.App\bin\Release\BasicApp.App.exe + + + + ..\..\packages\RhinoMocks.3.6.1\lib\net\Rhino.Mocks.dll + True + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/BasicApp.Test/Properties/AssemblyInfo.cs b/BasicApp.Test/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..3c8d28c --- /dev/null +++ b/BasicApp.Test/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("BasicApp.Test")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("BasicApp.Test")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("eaaee4a9-7516-4ab1-8a10-956c562c2cc6")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/BasicApp.Test/UtilitiesUnderTest.cs b/BasicApp.Test/UtilitiesUnderTest.cs new file mode 100644 index 0000000..8d211bd --- /dev/null +++ b/BasicApp.Test/UtilitiesUnderTest.cs @@ -0,0 +1,341 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; +using Rhino.Mocks; +using Rhino.Mocks.Constraints; +using BasicApp.App.Utilities; + +namespace BasicApp.Test.UtilitiesUnderTest +{ + [TestFixture] + class ProfessionUtilitiesUnderTest + { + private UtilitiesProfession _ObjUtil = null; + + [SetUp] + public void Setup() + { + _ObjUtil = new UtilitiesProfession(); + } + + [Test] + public void GetCompleteProfession_Return_SheIsASoftwareEngineer() + { + string StrResult = _ObjUtil.GetCompleteProfession("software engineer", UtilitiesProfession.Gender.Female); + StringAssert.AreEqualIgnoringCase("She is a software engineer", StrResult); + } + + [Test] + public void GetCompleteProfession_Return_HeIsAProjectManager() + { + string StrResult = _ObjUtil.GetCompleteProfession("software engineer", UtilitiesProfession.Gender.Male); + StringAssert.AreEqualIgnoringCase("He is a software engineer", StrResult); + } + + [Test] + public void GetCompleteProfession_Return_HeIsAnEngineer() + { + string StrResult = _ObjUtil.GetCompleteProfession("engineer", UtilitiesProfession.Gender.Male); + StringAssert.AreEqualIgnoringCase("He is an engineer", StrResult); + } + + [TearDown] + public void TearDown() + { + _ObjUtil = null; + } + } + + [TestFixture] + class DateUtilitiesUnderTest + { + private MockRepository _Mocks = null; + private UtilitiesDate _ObjUtil = null; + private TestableUtilitiesDate _ObjUtilTestable = null; + private StubReader _Reader = null; + + private string[] _ExpectedMonths = null; + private string _ExpectedMonthsAsString = null; + + [SetUp] + public void Setup() + { + _Mocks = new MockRepository(); + _ObjUtil = new UtilitiesDate(); + _ObjUtilTestable = new TestableUtilitiesDate(); + _Reader = new StubReader(); + + _ExpectedMonths = + new string[12] + {"January", "February", "March", + "April", "May", "June", + "July", "August", "September", + "October", "November", "December" + }; + + _ExpectedMonthsAsString + = string.Join("\r\n", _ExpectedMonths); + } + + [Test] + public void GetWeeks_Return_6() + { + decimal Weeks = _ObjUtil.GetWeeks(DateTime.Now.AddDays(-42), DateTime.Now); + Assert.AreEqual(6, Weeks); + } + + [Test] + public void GetDays_Return_25() + { + decimal Days = _ObjUtil.GetDays(DateTime.Now.AddDays(-25), DateTime.Now); + Assert.AreEqual(25, Days); + } + + [Test] + public void GetMonths_Returns_All_Months_InjectStub_ByParamPass() + { + string [] Months = _ObjUtil.GetMonthsStubByParamInject(_Reader); + Assert.AreEqual(_ExpectedMonths, Months); + } + + [Test] + public void GetMonths_Returns_All_Months_InjectStub_ByConstuctor() + { + _ObjUtil = new UtilitiesDate(_Reader); + string[] Months = _ObjUtil.GetMonthsStubByConstructorInject(); + Assert.AreEqual(_ExpectedMonths, Months); + } + + [Test] + public void GetMonths_Returns_All_Months_InjectStub_ByPropertyInject() + { + _ObjUtil.Reader = _Reader; + string[] Months = _ObjUtil.GetMonthsStubByPropertyInject(); + Assert.AreEqual(_ExpectedMonths, Months); + } + + [Test] + public void GetMonths_Returns_All_Months_InjectStub_UsingFactory() + { + ReaderFactory.SetReader(_Reader); + string[] Months = _ObjUtil.GetMonthsStubUsingFactory(); + Assert.AreEqual(_ExpectedMonths, Months); + } + + [Test] + public void GetMonths_Returns_All_Months_InjectStub_UsingLocalFactory() + { + TestableUtilitiesDate ObjUtil = new TestableUtilitiesDate(); + _ObjUtilTestable._Reader = _Reader; + string[] Months = _ObjUtilTestable.GetMonthsStubUsingFactory(); + Assert.AreEqual(_ExpectedMonths, Months); + } + + /* MANUALLY WRITTEN MOCKS + * + * Mocks, like stubs, need to be injected into the code. The very same strategies that were used for stubs + * can be used with mocks. Parameter, Constructor, Property, Factory, and Local Factory are all valid. Here + * is a test using manually written mocks. Here I use the local factory method. + */ + + [Test] + public void SetMonths_Proper_Content_Sent() + { + MockWriter Writer = new MockWriter(); + + _ObjUtilTestable._Writer = Writer; + + _ObjUtilTestable.SetMonths("emptyFile.txt"); + + Assert.AreEqual(_ExpectedMonthsAsString,Writer._Content); + } + + /* Isolation Frameworks are a great tool for saving time on having to manually create stubs and mocks. + * While they are rich in functionality it's still up to the programmer where mocks and stubs need to placed + * in the code. Poor placement of seams can lead to messy code and tests that are hard to understand. + */ + + + /* STRICT MOCKS + * + * Strict mock objects require that the set expectations be met completely for + * the test to pass. This means that if any recorded call differs in either parameters + * or name the test will fail immediately. It won't wait till verify is called. One + * thing to note here is that because ValidateAndWrite is expected to return something + * we need to specify in our test code what will be returned after that line. Remember that + * writer is an empty shell! + * + * Means of Failure: + * -an unexpected method is called + * -an expected method is never called + */ + + [Test] + public void RhinoMock_SetMonths_Proper_Content_Sent_Strict() + { + // initialization + IWriter Writer = _Mocks.StrictMock(); + _ObjUtilTestable._Writer = Writer; + + // expecatation + using (_Mocks.Record()) + { + Writer.ValidateAndWrite("emptyFile.txt", _ExpectedMonthsAsString); + LastCall.Return(true); + } + + // invokation + _ObjUtilTestable.SetMonths("emptyFile.txt"); + + // verification + _Mocks.Verify(Writer); + } + + /* NON-STRICT MOCKS + * + * Non-Strict mock objects require that the methods set in the expectation be called + * but it also allows other methods to be called. This makes the test less prone to + * failure. + * + * Means of Failure: + * -an expected method is never called + */ + + [Test] + public void RhinoMock_SetMonths_Proper_Content_Sent_NonStrict() + { + IWriter Writer = _Mocks.DynamicMock(); + _ObjUtilTestable._Writer = Writer; + + using (_Mocks.Record()) + { + Writer.ValidateAndWrite("emptyFile.txt",_ExpectedMonthsAsString); + LastCall.Return(true); + } + + _ObjUtilTestable.SetMonths("emptyFile.txt"); + + _Mocks.Verify(Writer); + } + + /* USE OF STUBS + * + * Don't the let the name Rhino Mock fool you! this framework is capable of creating + * stubs as well as mocks! + */ + + [Test] + public void RhinoMock_GetMonths_Returns_All_Months() + { + IReader Reader = _Mocks.Stub(); + _ObjUtilTestable._Reader = Reader; + + // We set expectations purely to tell the stub what to return. + using (_Mocks.Record()) + { + Reader.ValidateAndRead("file.txt"); + LastCall.Return(_ExpectedMonthsAsString); + } + + string[] Result = _ObjUtilTestable.GetMonthsStubUsingLocalFactory(); + + // If you verify the test will always pass, by definition stubs + // can't cause a test to fail, so we assert on something! + Assert.AreEqual(_ExpectedMonths, Result); + } + + /* CONSTRAINTS + * + * We can also test the object properties of our parameters! Check more docs on constraints as + * they offer very rich functionality to the test code by allowing us to set expectations on inputs. + * One thing to note is the use of overloaded AND/OR operators. + * + * Concern: One thing I didn't understand was that in the book "The Art of Unit Testing" + * the author would instantiate new instances of input in both the expectation and invokation, making them + * different. From my expierence it seems like you need to expect a specific instance. If a different + * instance is recieved in the invokation step the test will fail. Does this mean I have to inject my input + * as well as my Writer? For primitive types this wasn't the case. + * + * UPDATE: Now it works like in the book ... (Try and replicate error) + */ + + [Test] + public void RhinoMock_SetMonthsUsingWriterInput_Proper_Content_Sent() + { + IWriter Writer = _Mocks.DynamicMock(); + _ObjUtilTestable._Writer = Writer; + + using (_Mocks.Record()) + { + Writer.ValidateAndWrite(new WriterInput("emptyFile.txt", _ExpectedMonthsAsString)); + LastCall.Constraints( + Property.Value("Path", "emptyFile.txt") + && Property.Value("Content", _ExpectedMonthsAsString) + ).Return(true); + } + + _ObjUtilTestable.SetMonthsUsingWriterInput("emptyFile.txt"); + _Mocks.VerifyAll(); + } + + /* DELEGATES + * + * //// MISSING EXAMPLE //// + * + * Concern: I noticed that certain features are missing, I don't know if it's a version issue + * but my version of Rhino mock doesn't seem to have Is.Matching, So I couldn't introduce test code for delegates. + */ + + + /* AAA (ARRANGE, ACT, ASSERT) + * + * It maybe more intutive to design tests according to AAA rather than Record and Replay, which is what the above tests were + * using. Instead of setting expectations ahead of actually running our testable code we simply setup our test objects, + * run the code, and then assert that the stuff we expected to happen ... happened. This way of formating tests also requires + * the use of the lambda feature in C# so be wary if you aren't comfortable with functional language principles specifically + * how to use anonymous functions. + */ + [Test] + public void RhinoMock_SetMonths_Proper_Content_Sent_AAA() + { + // Arrange + IWriter Writer = _Mocks.DynamicMock(); + _ObjUtilTestable._Writer = Writer; + + // Act + _Mocks.ReplayAll(); + _ObjUtilTestable.SetMonths("emptyFile.txt"); + + // Assert + Writer.AssertWasCalled + (writer => writer.ValidateAndWrite + ("emptyFile.txt",_ExpectedMonthsAsString)); + } + + [Test] + public void RhinoMock_GetMonths_Returns_All_Months_AAA() + { + // Arrange + IReader Reader = _Mocks.Stub(); + _ObjUtilTestable._Reader = Reader; + + Reader.Expect(reader => reader.ValidateAndRead("file.txt")).Return(_ExpectedMonthsAsString); + + // Act + _Mocks.ReplayAll(); + string[] Result = _ObjUtilTestable.GetMonthsStubUsingLocalFactory(); + + // Assert + Assert.AreEqual(_ExpectedMonths, Result); + } + + [TearDown] + public void TearDown() + { + _ObjUtil = null; + _Reader = null; + } + } +} diff --git a/BasicApp.Test/packages.config b/BasicApp.Test/packages.config new file mode 100644 index 0000000..219b8d8 --- /dev/null +++ b/BasicApp.Test/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file