GOTCHA 29 Unit testing private methodstesting private methods


GOTCHA #29 Unit testing private methodstesting private methods

NUnit is an excellent tool that allows you to write unit-testing code, thereby improving the robustness of your application. (Refer to Gotcha #8, "Division operation isn't consistent between types" for a brief introduction to NUnit.) It serves as a critical aid in refactoringhelping you identify the code changes you need to make as your design evolves. Developers who have started using the tool find it hard to imagine writing any code and refactoring it without the test harness and the support provided by NUnit.

Where should your test cases go? The pundits recommend that you place the test classes (called the "test fixture") in the same assembly as the code you're testing. This allows you to test not only the public members of a class, but the internal members as well. While this sounds great, there is one problem. Doing so might force you to make some of the class's methods internal instead of private in order to test them. Also, just to make your class testable, sometimes you might find yourself writing methods that aren't otherwise needed. I've even heard the suggestion to use compiler flags to make a method internal for testing and private for release. Such options make the code less readable and result in some very unpleasant code-maintenance nightmares. Relaxing the access control for the sake of testing is also not desirable, especially when there is an alternative. Consider Example 3-22 to test a simple User class.

Example 3-22. NUnit test for a simple User

C# (TestPrivate)

 //Test.cs using System; using NUnit.Framework; using System.Security.Cryptography; namespace UnitTest {     [TestFixture]     public class Test     {         private User theUser;         [SetUp]         public void CreateUser()         {             theUser = new User();         }         [Test]         public void TestSetPassword()         {             string PASSWORD = "Cod!ng";              theUser.ChangePassword(null, PASSWORD);             // How do you assert that the password has been set?             // You can rely on calling the GetPassword method to do this.             // However, do you really want to provide a             // method to get the password?             // OK, let's write one for now.             byte[] hashCode = new SHA256Managed().ComputeHash(                 System.Text.Encoding.ASCII.GetBytes(PASSWORD));             string hashCodeString = BitConverter.ToString(hashCode);              Assert.AreEqual(hashCodeString, theUser.GetPassword());         }     } } //User.cs using System; using System.Security.Cryptography; namespace UnitTest {    public class User     {         private string password;         public void ChangePassword(                 string oldPassword, string thePassword)         {             // Make sure that the caller is either creating             // a new password, or knows the old password             if ((password == null && oldPassword == null)                 || CreateHash(oldPassword) == password)             {                 password = CreateHash(thePassword);             }             else             {                 throw new ApplicationException("Invalid password");             }         }         internal string GetPassword()         {             return password;         }         private string CreateHash(string input)         {             byte[] hashCode = new SHA256Managed().ComputeHash(                 System.Text.Encoding.ASCII.GetBytes(input));             return BitConverter.ToString(hashCode);         }     } } 

VB.NET (TestPrivate)

 'Test.vb Imports NUnit.Framework Imports System.Security.Cryptography <TestFixture()> _ Public Class Test     Private theUser As User     <SetUp()> _     Public Sub CreateCalculator()        theUser = New User     End Sub     <Test()> _   Public Sub TestSetPassword()         Dim PASSWORD As String = "Cod!ng"          theUser.ChangePassword(Nothing, PASSWORD)         'How do you assert that the password has been set?         'You can rely on calling the GetPassword method to do this.         'However, do you really want to provide a         'method to get the password?         'OK, let's write one for now.         Dim hashCode() As Byte = New SHA256Managed().ComputeHash( _                      System.Text.Encoding.ASCII.GetBytes(PASSWORD))         Dim hashCodeString As String = BitConverter.ToString(hashCode)          Assert.AreEqual(hashCodeString, theUser.GetPassword())     End Sub End Class 'User.vb Imports System Imports System.Security.Cryptography Public Class User     Private password As String     Public Sub ChangePassword(ByVal oldPassword As String, _             ByVal thePassword As String)         'Make sure that the caller is either creating a new password,         'or knows the old password         If (password Is Nothing And oldPassword Is Nothing) OrElse _                 CreateHash(oldPassword) = password Then             password = CreateHash(thePassword)         Else             Throw New ApplicationException("Invalid password")         End If     End Sub     Friend Function GetPassword() As String         Return password     End Function     Private Function CreateHash(ByVal input As String) As String         Dim hashCode() As Byte = New SHA256Managed().ComputeHash( _                      System.Text.Encoding.ASCII.GetBytes(input))         Return BitConverter.ToString(hashCode)     End Function End Class 

In this example, you have a User class that needs to be tested. You are writing a test case for the SetPassword() method. After the call to SetPassword(), you want to check if the password has been set correctly. How do you do that? From within the Test class, you can access the public members and internal/friend members of the User class (since Test is in the same assembly as User). The only option I can think of here is to write an internal/friend method named GetPassword() in the User class to make SetPassword() testable. This might not be desirable. You might not want to expose the password. Furthermore, you might not need GetPassword() in the application, since you are writing it just to test SetPassword().

Why not make the test case a nested class of the User class? (Of course, I am not suggesting that you make all test fixtures nested classes. But if you need your test case to access the private inner workings of a class, writing it as a nested class accomplishes this.) The modified code with Test as a nested class is shown in Example 3-23.

Example 3-23. Test as nested class

C# (TestPrivate)

 using System; using System.Security.Cryptography; using NUnit.Framework; namespace UnitTest {      public class User     {         private string password;         public void ChangePassword(             string oldPassword, string thePassword)         {             if ((password == null && oldPassword == null)                 || CreateHash(oldPassword) == password)             {                 password = CreateHash(thePassword);             }             else             {                throw new ApplicationException("Invalid password");             }         }         private string CreateHash(string input)         {             byte[] hashCode = new SHA256Managed().ComputeHash(                 System.Text.Encoding.ASCII.GetBytes(input));             return BitConverter.ToString(hashCode);         }         // In .NET 2.0, with Partial Classes, this can be         // in a separate file         [TestFixture]          public class Test         {             private User theUser;             [SetUp]             public void CreateUser()             {                 theUser = new User();             }             [Test]             public void TestSetPassword()             {                 string PASSWORD = "Cod!ng";                 theUser.ChangePassword(null, PASSWORD);                 Assert.AreEqual(theUser.password,                     theUser.CreateHash(PASSWORD));             }         }     } } 

VB.NET (TestPrivate)

 'User.vb Imports System Imports System.Security.Cryptography Imports NUnit.Framework  Public Class User     Private password As String     Public Sub ChangePassword(ByVal oldPassword As String, _             ByVal thePassword As String)         If (password Is Nothing And oldPassword Is Nothing) OrElse _                 CreateHash(oldPassword) = password Then             password = CreateHash(thePassword)         Else             Throw New ApplicationException("Invalid password")         End If     End Sub     Private Function CreateHash(ByVal input As String) As String         Dim hashCode() As Byte = New SHA256Managed().ComputeHash( _                      System.Text.Encoding.ASCII.GetBytes(input))         Return BitConverter.ToString(hashCode)     End Function     'In .NET 2.0, with Partial Classes, this can be in a separate file     <TestFixture()> _      Public Class Test         Private theUser As User         <SetUp()> _         Public Sub CreateCalculator()             theUser = New User         End Sub         <Test()> _       Public Sub TestSetPassword()             Dim PASSWORD As String = "Cod!ng"             theUser.ChangePassword(Nothing, PASSWORD)             Assert.AreEqual(theUser.password, _                 theUser.CreateHash(PASSWORD))         End Sub     End Class End Class 

In this code, the Test class is a nested class of the User class. Nested classes have full access to private members of the nesting class. This allows a more convenient way to test the class implementation without compromising the encapsulation or access control. The test case being executed under NUnit is shown in Figure 3-19.

One disadvantage of this approach is that the class file User.cs (or User.vb) now becomes larger. Furthermore, you may not want to release your test cases with your

Figure 3-19. NUnit test executing a nested test fixture


assembly. This will not be an issue in the next release of .NET, where partial classes are allowed. Then you'll be able to write the User class in one or more files and keep the test cases in different files. That will also make it easier to remove test cases from the build in production.

IN A NUTSHELL

If you find yourself changing the accessibility of fields and methods just so you can test them, or you start introducing methods only for the sake of testing other methods (e.g., GetPassword() in Example 3-22), consider writing those tests as nested classes. The tests that depend only on non-private members of a class should still be written as higher-level classes in the same project.

SEE ALSO

Gotcha #8, "Division operation isn't consistent between types."



    .NET Gotachas
    .NET Gotachas
    ISBN: N/A
    EAN: N/A
    Year: 2005
    Pages: 126

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net