Test Automation Code Review Guidelines

This document is primarily meant for code reviewers and is an overview of best practices for code authors. Test code has a high maintenance cost associated with it. The better the quality, the lower the cost. Test code reviews help ensure that quality and standards are shared across the project team.  Getting the test code looked at by another set  or more sets of eyes will not only improve the quality of the test code, but it can also  expose any assumptions made by the developer and help in generating more test cases. The most common problems to look for during a test code review can be categorized into buckets: 

Code review defect categories 

Duplicate code - code that is the same or almost the same or even similar to code elsewhere. Consider refactoring or if possible, Data Driven Testing. Some examples of duplicate code are described below:

  1. Creating 2 separate test case methods for similar test conditions such as min/max values, nulls or other boundary conditions for parameters. Instead, extract out the common logic in the two test cases and make it a method that both can call.

    Code Sample – Creating 2 separate tests for similar conditions and repeating validation logic in both tests

    Not so good: Repeating validation logic in multiple test case methods

    The following code block shows multiple test methods repeating the same/similar validation logic. This can instead be extracted and made into a separate method which both the tests can call.

    [TestMethod]
    [Description("Validations for Customer Name member field")]
    public void ValidateCustomerName(string name)
    {
        if (string.IsNullOrEmpty(name))
        {
            Assert.Fail("Test Failed: Customer Name is Empty");
        }
        if (name.Length < 20)
        {
            Assert.Fail("Test Failed: Customer Name length less than 20");
        }
        if (name.Length > 50)
        {
            Assert.Fail("Test Failed: Customer Name length greater than 50");
        }
    }
    [TestMethod]
    [Description("Validations for Customer Title member field")]
    public void ValidateCustomerTitle(string title)
    {
        if (!string.IsNullOrEmpty(title))
        {
            Assert.Fail("Test Failed: Customer Title is Empty");
        }
        if (title.Length < 20)
        {
            Assert.Fail("Test Failed: Customer Title length less than 20");
        }
        if (title.Length > 100)
        {
            Assert.Fail("Test Failed: Customer Title length greater than 100");
        }
    }

    Best practice: Extract common logic and make a separate function which others can invoke

    The code block below demonstrates creation of a separate validation function which the 2 tests can invoke

    [TestMethod]
    [Description("Validations for Customer Name member field")]
    public void ValidateCustomerName(string name)
    {
        ValidateField(name, "Customer Name", 20, 50);
    }
    [TestMethod]
    [Description("Validations for Customer Title member field")]
    public void ValidateCustomerTitle(string title)
    {
        ValidateField(title, "Customer Title",20,100);
    }
    /// <summary>
    /// Method to validate a member field value
    /// </summary>
    /// <param name="Value"></param>
    /// <param name="fieldName"></param>
    public void ValidateField(string value, string fieldName, int min, int max)
    {
        if (string.IsNullOrEmpty(value))
        {
            Assert.Fail(string.Format("Test Failed: {0} is Empty",fieldName));
        }
        if (value.Length < min)
        {
            Assert.Fail(string.Format("Test Failed: {0} length less than {1}", fieldName, min));
        }
        if (value.Length > max)
        {
            Assert.Fail(string.Format("Test Failed: {0} length greater than {1}", fieldName, max));
        }
    }

    Creating test code which tests functionality already tested in a different test method. For example testing a "get" along with a “set” and also having a separate test method for testing the "get". In this case, the test method that only tests "get" can be removed.

    Consider the following Customer Class for the following samples

    /// <summary>
    /// Sample Customer Class for demoing best practices
    /// </summary>
    public class Customer
    {
        public Guid ID;
        public string Title;
        public string Name;
    }

    Code Sample – Avoiding Repeating the same tests in multiple test cases

    Not so good: Repeating the same validations in multiple test cases

    The following code block show how multiple test cases repeat the same validations. This practice causes redundant code to be written and should be avoided.

    /// <summary>
    /// Method to validate a member field value, used by the Test Methods
    /// ValidateCustomerTitle, ValidateCustomerName below
    /// </summary>
    /// <param name="Value"></param>
    /// <param name="fieldName"></param>
    public void ValidateField(string value, string fieldName, int min, int max)
    {
        if (string.IsNullOrEmpty(value))
        {
            Assert.Fail(string.Format("Test Failed: {0} is Empty",fieldName));
        }
        if (value.Length < min)
        {
            Assert.Fail(string.Format("Test Failed: {0} length less than {1}", fieldName, min));
        }
        if (value.Length > max)
        {
            Assert.Fail(string.Format("Test Failed: {0} length greater than {1}", fieldName, max));
        }
    }
    Test Method 1:
    [TestMethod]
    [Description("Validations for Customer Name member field")]
    public void ValidateCustomerName(string name)
    {
        ValidateField(name, "Customer Name", 20, 50);
    }
    Test Method 2:
    [TestMethod]
    [Description("Validations for Customer Title member field")]
    public void ValidateCustomerTitle(string title)
    {
        ValidateField(title, "Customer Title",20,100);
    }
    Test Method 3:
    [TestMethod]
    [Description("Validate Customer objects properties are set successfully")]
    public void ValidateCustomer()
    {
        //Declare and initialize a Customer object
        Customer customer = new Customer();
        Guid customerId = Guid.NewGuid();
        customer.ID = customerId;
        customer.Name = "John Doe";
        customer.Title = "CEO";
        //Verification of the Customer object member fields
        if (customer.ID == null)
        {
            Console.Write("Test Failed – Customer ID not set successfully");
        }
        ValidateField(customer.Name, "Customer Name", 20, 50);
        ValidateField(customer.Title, "Customer Title", 20, 100);
    }

    Best practice: Having test cases implement validations only once and avoid redundancy

    The code block below demonstrates having validations implemented only once and not being repeated multiple times.

    [TestMethod]
    [Description("Validate Customer objects properties are set successfully")]
    public void ValidateCustomer()
    {
        //Declare and initialize a Customer object
        Customer customer = new Customer();
        Guid customerId = Guid.NewGuid();
        customer.ID = customerId;
        customer.Name = "John Doe";
        customer.Title = "CEO";
        //Verification of the Customer object member fields
        if (customer.ID == null)
        {
            Console.Write("Test Failed – Customer ID not set successfully");
        }
        ValidateField(customer.Name, "Customer Name", 20, 50);
        ValidateField(customer.Title, "Customer Title", 20, 100);
    }

     

  2. Testing the same functionality dropped in 2 different releases by rewriting existing test cases just for the new release. Existing test code should be written in a way that it works across multiple releases.

    Code Sample – Writing test code and having it work across multiple releases

    Not so good: Writing new test code for every release/version

    The following code block shows new code being written for every new release of the product being tested. In most cases this causes the previously written code and validations to go to waste and requires more time and effort to write the new code.

    V1 Code

     [TestMethod]
    [Description("Validate Customer objects properties are set successfully - V1")]
    public void ValidateCustomerV1()
    {
        //Declare and initialize a Product.Release1.Customer object
        Product.Release1.Customer customer = new Product.Release1.Customer();
        Guid customerId = Guid.NewGuid();
        customer.ID = customerId;
        customer.Name = "John Doe";
        customer.Title = "CEO";
        //Verification of the Customer object member fields
        if (customer.ID == null)
        {
            Console.Write("Test Failed – Customer ID not set successfully");
        }
        ValidateField(customer.Name, "Customer Name", 20, 50);
        //ValidateField method defined above in example 2

        ValidateField(customer.Title, "Customer Title", 20, 100);
       //ValidateField method defined above in example 2
    }
    V2 Code:
    [TestMethod]
    [Description("Validate Customer objects properties are set successfully – V2")]
    public void ValidateCustomerV2()
    {
        //Declare and initialize a Product.Release2.Customer object
        Product.Release2.Customer customer = new Product.Release2.Customer();
        Guid customerId = Guid.NewGuid();
        customer.ID = customerId;
        customer.Name = "John Doe";
        customer.Title = "CEO";
        //Verification of the Customer object member fields
        if (customer.ID == null)
        {
            Console.Write("Test Failed – Customer ID not set successfully");
        }
        ValidateField(customer.Name, "Customer Name", 20, 50);
       //ValidateField method defined above in example 2

        ValidateField(customer.Title, "Customer Title", 20, 100);
       //ValidateField method defined above in example 2
    }
     

    Best practice: Getting the code to work across multiple releases – use of compiler directives

    The code block below demonstrates the use of compiler directives to get the code to work across multiple releases.

    V1, V2 Code:

     [TestMethod]
    [Description("Validate Customer objects properties are set successfully ")]
    public void ValidateCustomer()
    {
        //Declare and initialize a Customer object
    #if v1
        Product.Release1.Customer customer = new Product.Release1.Customer ();
    #elif v2
        Product.Release2.Customer customer = new Product.Release2.Customer();
    #endif
        Guid customerId = Guid.NewGuid();
        customer.ID = customerId;
        customer.Name = "John Doe";
        customer.Title = "CEO";
        //Verification of the Customer object member fields
        if (customer.ID == null)
        {
            Console.Write("Test Failed – Customer ID not set successfully");
        }
        ValidateField(customer.Name, "Customer Name", 20, 50);
       //ValidateField method defined above in example 2

        ValidateField(customer.Title, "Customer Title", 20, 100);
       //ValidateField method defined above in example 2
    }
     
  3. If an existing test case changes from one release to another, the use of pre-compile directives can be employed. Or, if the test case becomes fundamentally different then a new test case could be written.

    Code Sample – Handling test case changes from one release to another in test automation

    Not so good: Writing new test code for each new release

    The following code block shows writing new test methods for each new release when there are minor changes to the test case validations. This can be avoided by using conditional compiler directives.

    Release 1:

     [TestMethod]
    [Description("Validate Customer objects properties are set successfully - V1")]
    public void ValidateCustomerV1()
    {
        //Declare and initialize a Customer object
        Customer customer = new Customer();
        Guid customerId = Guid.NewGuid();
        customer.ID = customerId;
        customer.Name = "John Doe";
        customer.Title = "CEO";
        //Verification of the Customer object member fields
        if (customer.ID == null)
        {
            Console.Write("Test Failed – Customer ID not set successfully");
        }
        ValidateField(customer.Name, "Customer Name", 20, 50);
        ValidateField(customer.Title, "Customer Title", 20, 100);
    }

    Release 2:

    [TestMethod]
    [Description("Validate Customer objects properties are set successfully – V2")]
    public void ValidateCustomerV2()
    {
        //Declare and initialize a Customer object
        Customer customer = new Customer();
        Guid customerId = Guid.NewGuid();
        customer.ID = customerId;
        customer.Name = "John Doe";
        customer.Title = "CEO";
        //Verification of the Customer object member fields, new Verification added in Release 2
        if (customer.ID == null || customer.ID.ToString() == "00000000-0000-0000-0000-000000000000")
        {
            Console.Write("Test Failed – Customer ID not set successfully");
        }
        //Verification of the Customer object member fields Changed in Release 2
        ValidateField(customer.Name, "Customer Name", 30, 100);
       //ValidateField method defined above in example 2

        ValidateField(customer.Title, "Customer Title", 30, 200);
       //ValidateField method defined above in example 2
    }

    Best practice: Using Compiler directives to apply appropriate Field validations based on release being tested.

    V1, V2 Code:

    [TestMethod]
    [Description("Validate Customer objects properties are set successfully ")]
    public void ValidateCustomer ()
    {
        //Declare and initialize a Customer object
        Customer customer = new Customer ();
        Guid customerId = Guid.NewGuid();
        customer.ID = customerId;
        customer.Name = "John Doe";
        customer.Title = "CEO";
    #if v1
        //Verification of the Customer object member fields
        if (customer.ID == null)
        {
            Console.Write("Test Failed – Customer ID not set successfully");
        }
        ValidateField(customer.Name, "Customer Name", 20, 50);
       //ValidateField method defined above in example 2

        ValidateField(customer.Title, "Customer Title", 20, 100);
       //ValidateField method defined above in example 2
    #elif v2
        //Verification of the Customer object member fields
        if (customer.ID == null || customer.ID.ToString() == "00000000-0000-0000-0000-000000000000")
        {
            Console.Write("Test Failed – Customer ID not set successfully");
        }
        ValidateField(customer.Name, "Customer Name", 30, 100);
       //ValidateField method defined above in example 2

        ValidateField(customer.Title, "Customer Title", 30, 200);
       //ValidateField method defined above in example 2
    #endif
    }

     

Duplicating functionality - Re-writing existing libraries (either test libraries, tools, or .Net framework libraries) is expensive to write and maintain. Ask yourself often: "Could there already be existing code already written that will do what I need?" Examples of this include:

  1. Creating a separate random or project data generation when a new product revision is released. Instead, data generation libraries should be re-used as much as possible across projects.

    Code Sample – Reusing the existing libraries across project releases

    Not so good: Writing  a new test data generation method for every project release being tested.

    The following code shows a new Data Generation library being implemented for every release. This causes lot of redundant library code and should be avoided by reusing the Data library across releases.

    Consider the following Customer Class for the following samples

    /// <summary>
    /// Sample Customer Class for demoing best practices
    /// </summary>
    public class Customer
    {
        public Guid ID;
        public string Name;
    }

    V1 Code:

    /// <summary>
    /// Class definition for DataLibraryV2 to generate Data for V2
    /// </summary>
    public class DataLibraryV2
    {
        /// <summary>
        /// Generate Customer ID for V2
        /// </summary>
        /// <returns></returns>
        public static Guid V2GenerateCustomerId()
        {
            return Guid.NewGuid();
        }
        /// <summary>
        /// Generate Customer Name for V2
        /// </summary>
        /// <returns></returns>
        public static string V2GenerateCustomerName()
        {
            return "John Doe";
        }
    }
    /// <summary>
    /// Initialize the Customer object instance for Release V2
    /// </summary>
    public Customer InitCustomer()
    {
        //Declare and initialize a CustomerV1 object
        Customer customer = new Customer();
        customer.ID = DataLibraryV2.V2GenerateCustomerId();
        customer.Name = DataLibraryV2.V2GenerateCustomerName();
        return customer;
    }

    Best practice: Reusing Data generation libraries across project releases

    The code block below demonstrates use of the same Data generation library across project releases.

    /// <summary>
    /// Sample Class for generating test data
    /// </summary>
    public class DataLibrary
    {
        /// <summary>
        /// Generate Customer ID
        /// </summary>
        /// <returns></returns>
        public static Guid GenerateCustomerId()
        {
            return Guid.NewGuid();
        }
        /// <summary>
        /// Generate Customer Name
        /// </summary>
        /// <returns></returns>
        public static string GenerateCustomerName()
        {
            return "John Doe";
        }
    }

    V1 Code,V2 Code:

    /// <summary>
    /// Initialize the Customer object instance
    /// </summary>
    public Customer InitCustomer()
    {
        //Declare and initialize a Customer object
        Customer customer = new Customer();
        customer.ID = DataLibrary.GenerateCustomerId();
        customer.Name = DataLibrary.GenerateCustomerName();
        return customer;
    }
  2. Writing separate releases of library code to handle multiple different namespaces of the same product. Pre-processor directives or reflections or generics should be used to make code independent of project namespaces. This applies specifically to code which will be used to test multiple releases of the product.

    Code Sample – Writing separate test code to handle multiple namespaces

    Not so good: Writing new test code to handle namespaces between releases

    The following code block shows new test code being written to handle namespace changes across product releases. This style of programming can cause scores of lines of code to be written which ceases to be utilized after the release. This approach can easily be avoided by the use of Compiler Directives.

    V1 Code:

    namespace Product.Release1
    {
        /// <summary>
        /// Sample Customer Class for demoing best practices
        /// </summary>
        public class Customer
        {
            public Guid ID;
            public string Name;
        }
    }
    using Product.Release1;
    /// <summary>
    /// Initialize the Customer object instance
    /// </summary>
    public Product.Release1.Customer InitCustomer()
    {
        //Declare and initialize a CustomerV1 object
        Product.Release1.Customer customer = new Product.Release1.Customer();
        customer.ID = Guid.NewGuid();
        customer.Name = "John Doe";
        return customer;
    }

    V2 Code:

    namespace Product.Release2
    {
        /// <summary>
        /// Sample Customer Class for demoing best practices
        /// </summary>
        public class Customer
        {
            public Guid ID;
            public string Name;
            public bool x;
        }
    }
    using Product.Release2
    /// <summary>
    /// Initialize the Customer object instance
    /// </summary>
    public Product.Release2.Customer InitCustomer()
    {
        //Declare and initialize a Customer object
        Product.Release2.Customer customer = new Product.Release2.Customer();
        customer.ID = Guid.NewGuid();
        customer.Name = "John Doe";
        return customer;
    }

    Best practice: Handling namespace changes across product releases.

    The code block below demonstrates the test code handling namespace changes by use of Compiler directives. The test code resolves the Customer object to the appropriate namespace as per the Compiler directive setting.

    V1, V2 Code:

    #if release1
          using Product.Release1; //Defined above
    #elif release2
          using Product.Release2; //Defined above
    #endif
    /// <summary>
    /// Initialize the Customer object instance
    /// </summary>
    public Customer InitCustomer()
    {
        //Declare and initialize a Customer object
        Customer customer = new Customer();
        customer.ID = DataLibrary.GenerateCustomerId();
        customer.Name = DataLibrary.GenerateCustomerName();
        return customer;
    }
  3. Re-writing functions which have part of the same functionality implemented by other methods should be avoided. For example, if functionality involves opening a file, reading a value from it, connecting to a database and getting values out and if methods exist for each of these tasks then they should be reused instead of creating a new method which does all 3 tasks. (i.e. Patterns & Practices Application Block for SQL).

    Code Sample – Avoiding Duplicating Functionality

    Not so good : Writing our own function to format strings

    The code block below shows string formatting using the System.String object, this is not so effective approach to format a number of strings. Instead the built-in functions of the string class can be used instead.

    //Initialize name variables
    string title = "Mr.", firstName = "John", middleInitial = "D", lastName = "Smith";
    //Writing our own GetFullName function to retrieve the full name
    fullName = this.GetFullName(title, firstName, middleInitial, lastName);
    private string GetFullName(string title, string firstName, string middleInitial, string lastName)
    {
        string fullName = title;
        fullName += " " + firstName;
        fullName += " " + middleInitial;
        fullName += " " + lastName;
        return fullName;
    }

    Best practice: Using built in CLR function to format strings

    The code block below shows using built in functions to perform some common tasks like string concatenation.

    //Use the built in system.string Format method to format the Name string to appropriate output
    string fullName  = string.Format("{0} {1} {2} {3}", title, firstName, middleInitial, lastName)

Incorrect Exception handling – “swallowing” exceptions is the worst practice, but even solely logging is bad. In general, test code should not need exception handling, as the underlying test harness will automatically deal with it. The exception is if you have significant information to add to the exception message, and in that case, the original exception should be passed as an InnerException.

For negative testing, if you are testing for a certain exception, catching and validating is required as VSTS does not check the actual message string when put in the test case attribute. In test library code it may be necessary (if no Try* method exist) to handle some exceptions. 

Code Sample - Handling Exceptions
The below code shows an example of swallowing exceptions which causes the Test being run having no way of knowing what exception was raised and who raised it if it runs into any issues. This practice causes nightmares during troubleshooting and should be avoided at all times.

Not so good: Swallowing exceptions thrown during a program execution flow

try
{
   ConnectionStringsSection connStringSection = (ConnectionStringsSection)
ConfigurationManager.GetSection ("connectionStrings");
   string connectionString = connStringSection.ConnectionStrings[connstringnode].ToString();
}
catch (Exception e)
{
}

The code block below is an example of handling exceptions thrown during the code execution flow.

Best practice:  Catch exceptions with multiple catches to ensure all known exceptions types are handled appropriately.

try
{
    ConnectionStringsSection connStringSection = (ConnectionStringsSection)
ConfigurationManager.GetSection("connectionStrings");
    string connectionString = connStringSection.ConnectionStrings[connstringnode].ToString();
}
catch (ConfigurationException configurationException)
{
    throw new ApplicationException("Please check that the config file is not
missing and that it has a connectionStrings section.", configurationException);
}

 

System resource leaks - This is especially common when using SqlConnection, SqlCommand etc. statements. The best way to use these is to wrap it with a "using" statement. Anything that implements IDisposable should be wrapped in this statement.

  1. Any resources used should be opened pessimistically, i.e. they should be opened in such a way that they are automatically closed once the usage is complete. This includes opening files in read-only mode unless read/write is required, and closing database connections immediately after usage.

    Code Sample – ‘Using’ Statements for Automatic connection closure/cleanup

    Not so good: SqlConnection/SqlReader objects not being closed thus causing unreleased memory/system resource leaks.

    The code block below shows the issues when System resources like SQL Connection, command objects are not closed correctly after usage. This style of programming should be avoided and all resources should be appropriately closed after use.

    string connectionString = "Data Source= MyAppSqlServer;Initial Catalog=MyAppDatabase;
    Integrated Security=SSPI;";
    string commandString = "SaveCustomer";
    SqlConnection connection = new SqlConnection(connectionString);
    connection.Open();
    SqlCommand command = new SqlCommand(commandString);
    {
        command.CommandType = CommandType.StoredProcedure;
        command.Connection = connection;
        command.Parameters.Add("@CustomerId", SqlDbType.VarChar, 80).Value = "102903909940";
        command.Parameters.Add("@CustomerName", SqlDbType.VarChar, 100).Value = "John Doe Smith";
        SqlDataReader reader = command.ExecuteReader();
        while (reader.Read())
        {
            //Do something
        }
    }

    Best practice: Wrap SqlConnection objects and resources within using statements to ensure proper cleanup and memory release

    The code block below is an example of wrapping Connection/Command object usage within ‘using’ statements. The using statement allows the programmer to specify when objects that use resources should release them. The object provided to the using statement must implement the IDisposable interface. This interface provides the Dispose method, which should release the object's resources.

    //Enclose the code opening the SqlConnection, SqlCommand, SqlReader object within "Using" 
    statements to ensure they are automatically closed and cleaned up once the usage is complete
    string connectionString = "Data Source= MyAppSqlServer;Initial Catalog=MyAppDatabase;
    Integrated Security=SSPI;";
    string commandString = "SaveCustomer";
    using (SqlConnection connection = new SqlConnection(connectionString))
    {
        connection.Open();
        using (SqlCommand command = new SqlCommand(commandString))
        {
            command.CommandType = CommandType.StoredProcedure;
            command.Connection = connection;
            command.Parameters.Add("@CustomerId",  SqlDbType.VarChar, 80).Value = "102903909940";
            command.Parameters.Add("@CustomerName", SqlDbType.VarChar,100).Value = "John Doe Smith";
            using (SqlDataReader reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    //Do something
                }
            }
        }
    }
  2. In the case of unmanaged pointers, system resources need to be properly released. It is a good design approach to implement a SafeHandle derived class to manage this for you. This would force you to implement the releasehandle code which can include memory clean-up related code.

    Code Sample – Using SafeHandle to free up resources in case of unmanaged pointers.

    This best practice is demoed by using SafeHandleZeroOrMinusOneIsInvalid Class. SafeHandle Provides a base class for Win32 safe handle implementations in which the value of either 0 or -1 indicates an invalid handle.

    public abstract class SafeHandleZeroOrMinusOneIsInvalid : SafeHandle

     The following code example demonstrates how to create a class that derives from the SafeHandleZeroOrMinusOneIsInvalid class. This example creates a class that wraps a pointer to unmanaged memory.

    public static void Main()
    {
        IntPtr intPtr = Marshal.AllocHGlobal(10);
        Trace.WriteLine("Ten bytes of unmanaged memory allocated.");
        SafeUnmanagedMemoryHandle safeUhandle = new SafeUnmanagedMemoryHandle(intPtr, true);
        if (safeUhandle.IsInvalid)
        {
            Trace.WriteLine("SafeUnmanagedMemoryHandle is invalid.");
        }
        else
        {
            Trace.WriteLine("SafeUnmanagedMemoryHandle class successfully initialized to unmanaged memory.");
        }
    }

FakePre-5c68222dbfa143bc8d7da3df91bfd48c-28d37553949a4afe9ce09fb852b7883f

  1. Un-managed code usage should be avoided as much as possible and C# should be used as much as possible. If unavoidable, then usage should be called out and clearly documented.

    Code Sample – Usage of Unmanaged code within C#

    Not so good: Usage of Unmanaged Code from within C#, undocumented code is hard to understand and maintain.

    The following code block is an example to show how to use UnManaged code from within C# code and shows the minimum requirements for declaring a C# method that is implemented in an unmanaged Dll. This example does not clearly call out the usage of unsafe code within this class and is not documented.

     using System.Text;
    using System.Security.Permissions;
    using System.Runtime.InteropServices;
    class DirectoryTest
     {
         [DllImport("kernel32", SetLastError = true)]
         static extern bool CreateDirectory(string name, SecurityAttribute sa);
         [DllImport("kernel32", SetLastError = true)]
         static extern bool RemoveDirectory(string name);
         public static void Main()
         {
             SecurityAttribute sa = new SecurityPermissionAttribute(SecurityAction.Assert);
             bool flag = CreateDirectory("..\\..\\TestDir", null);
             if (flag)
             {
                 Trace.Write("Directory successfully created");
                 RemoveDirectory("..\\..\\TestDir");
             }
             else
             {
                 Trace.Write("Directory creation failed");
             }
         }
    }

    Best practice: Avoid usage of unmanaged code in C#, if unavoidable then clearly call out and document the usage.

    The code block below is an example to clearly call out and document the usage of UnManaged code within C#. The methods CreateDirectory and RemoveDirectory are declared with static and extern modifiers and have the DllImport attribute which tells the compiler that the implementation comes from kernel32.dll.

    using System.Text;
    using System.Security.Permissions;
    using System.Runtime.InteropServices;
    class DirectoryTest
     {
         #region Using Unmanaged Code
         /// <summary>
         /// External Method definition for CreateDirectory
         /// The DllImport attribute tells the compiler that the implementation of the
         /// CreateDirectory method comes from kernel32.dll
         /// </summary>
         /// <param name="c"></param>
         /// <returns></returns>
         [DllImport("kernel32", SetLastError = true)]
         static extern bool CreateDirectory(string name, SecurityAttribute sa);
         /// <summary>
         /// External Method definition for RemoveDirectory
         /// The DllImport attribute tells the compiler that the implementation of the
         /// RemoveDirectory method comes from kernel32.dll
         /// </summary>
         /// <returns></returns>
         [DllImport("kernel32", SetLastError = true)]
         //Static and extern modifiers indicate this method implementation is external to
         /// this class definition and can be invoked like any other static member
         static extern bool RemoveDirectory(string name);
         #endregion Using Unmanaged Code
         public static void Main()
         {
             SecurityAttribute sa = new SecurityPermissionAttribute(SecurityAction.Assert);
        //Create the Directory TestDir 2 levels up from the execution folder
        //Use the win32 CreateDirectory method to Create the directory
             bool flag = CreateDirectory("..\\..\\TestDir", null);
             if (flag)
             {
                 Trace.Write("Directory successfully created");
            //Remove the Directory TestDir from the execution folder once execution is complete.
                   //Use the win32 RemoveDirectory method to delete the directory
                 RemoveDirectory("..\\..\\TestDir");
             }
             else
             {
                 Trace.Write("Directory creation failed");
             }
         }
    }
     

     

Non-use of verification functionality - Test harnesses like VSTS have built-in verifications which are quick and easy to add. Test code should not throw exceptions but rather use Assert.* methods, and Assert.Fail() if nothing else matches. Don't hesitate to incorporate Assert.* calls into your libraries - some of the most efficient test cases relegate all verifications to library methods. All test cases should rely on some form of verification.

For all the following 3 examples below consider the following Customer class

/// <summary>
/// Sample Customer Class definition
/// </summary>
public class Customer
{
    public Guid ID;
    public string Name;
    public bool IsActive;
    public string[] Emails;
    public DateTime CreatedDate;
    public DateTime UpdatedDate;
}
  1. All verification methods should use the built in VSTS assert classes.

    Code Sample - Use of Assertions to validate a test case

    The following code block uses if statements to validate a test case which work fine if we have very few validations to do. But in case of multiple validations it becomes lengthy to write and maintain and can be replaced by the better Assert statement.

    [TestMethod]
    [Description("Validate Customer objects properties are set successfully")]
    public void ValidateCustomer()
    {
        //Declare and initialize a Customer object
        Customer customer = new Customer();
        Guid customerId = Guid.NewGuid();
        customer.ID = customerId;
        customer.Name = "John Doe";
        customer.IsActive = true;
        customer.CreatedDate = "2/6/2008 9:46:07 PM";
        customer.UpdatedDate = "2/6/2008 9:46:07 PM";
        customer.Emails = new string[2] { "John@company1.com", "Johndoe@company2.com" };
        //Verification of the Customer object using if statements
        if (customer.ID == null)
        {
            Console.Write("Test Failed – Customer ID not set successfully");
        }
        if (!customer.IsActive)
        {
            Console.Write("Test Failed – Customer IsActive Field not set successfully");
        }
        if (customer.Name == String.Empty)
        {
            Console.Write("Test Failed – Customer Name empty");
        }
        if (customer.Name != "John Doe")
        {
            Console.Write("Test Failed – Customer Name not set correctly");
        }
        if (customer.CreatedDate != "2/6/2008 9:46:07 PM")
        {
            Console.Write("Test Failed – Customer CreatedDate not set correctly");
        }
        if (customer.UpdatedDate != "2/6/2008 9:46:07 PM")
        {
            Console.Write("Test Failed – Customer UpdateDate not set correctly");
        }
        if (customer.Emails[0] != "John@Company1.com")
        {
            Console.Write("Test Failed – Customer Email1 not set correctly");
        }
        if (customer.Emails[1] != "Johndoe@Company2.com")
        {
            Console.Write("Test Failed – Customer Email2 not set correctly");
        }
    }

    Best practice: Using Assert statements to perform validations ensures code is cleaner and more readable in the long run

    The code block below is an example of using Assert statements to perform test verifications and validations. This is also an example to show verifications should be performed for all possible fields for the object being tested to ensure appropriate coverage for the object.

    [TestMethod]
    [Description("Validate Customer objects properties are set successfully")]
    public void ValidateCustomerAllFields()
    {
        //Declare and initialize a Customer object
        Customer customer = new Customer();
        Guid customerId = Guid.NewGuid();
        customer.ID = customerId;
        customer.Name = “John Doe”;
        customer.IsActive = true;
        customer.CreatedDate = "2/6/2008 9:46:07 PM";
        customer.UpdatedDate = "2/6/2008 9:46:07 PM";
        customer.Emails = new string[2] {"John@company1.com", "Johndoe@company2.com"};
        //Validations of the Customer object member fields
        Assert.AreNotEqual(null, customer.ID, "Test Failed – Customer ID not set successfully");
        Assert.IsTrue(customer.IsActive, "Test Failed – Customer IsActive Field not set successfully");
        Assert.AreEqual(customer.Id,customerId);
        Assert.AreNotEqual(string.Empty,customer.Name, "Test Failed – Customer Name empty");
        Assert.AreEqual(customer.Name, “John Doe”);
        Assert.AreEqual(customer.CreatedDate, “2/6/2008 9:46:07 PM”);
        Assert.AreEqual(customer.UpdatedDate, “2/6/2008 9:46:07 PM”);
        //Invoke a separate validation function to validate the Emails field
        Customer.VerifyCustomerEmails(customer);
    }
    /// <summary>
    /// Validate Emails field of the Customer object
    /// </summary>
    /// <param name="customer"></param>
    public static void ValidateCustomerEmails(Customer customer)
    {
        Assert.AreNotEqual(customer.Emails[0], string.Empty);
        Assert.AreNotEqual(customer.Emails[1], string.Empty);
        Assert.AreEqual(customer.Emails[0], "John@Company1.com");
        Assert.AreEqual(customer.Emails[1], "Johndoe@Company2.com");
    }
  2. Verifications should be performed for all possible fields of the object being tested. This should include core fields and also audit fields such as created, updated date etc.

    Code Sample – Validations for all member fields of an object

    Not so good: Incomplete validations on the member fields of the object under test

    The following code block is an example of incomplete validations on the member fields of the Customer object. Incomplete validations in the test automation lead to false positives and ultimately result in bugs elsewhere in the product.

     [TestMethod]
    [Description("Validate Customer objects properties are set successfully")]
    public void ValidateCustomer()
    {
        //Declare and initialize a Customer object
        Customer customer = new Customer();
        Guid customerId = Guid.NewGuid();
        customer.ID = customerId;
        customer.Name = "John Doe";
        customer.IsActive = true;
        customer.CreatedDate = "2/6/2008 9:46:07 PM";
        customer.UpdatedDate = "2/6/2008 9:46:07 PM";
        //Verification of the Customer object member fields
        Assert.AreNotEqual(null, customer.ID, "Test Failed – Customer ID not set successfully");
        Assert.AreNotEqual(string.Empty,customer.Name, "Test Failed – Customer Name empty");
    }
     

    Best practice: Performing complete validations on all fields of the object under test

    The code block below is an example to show validations should be performed for all possible fields for the object being tested to ensure appropriate coverage for the object.

     [TestMethod]
    [Description("Validate Customer objects properties are set successfully")]
    public void VerifyCustomerAllFields()
    {
        //Declare and initialize a Customer object
        Customer customer = new Customer();
        Guid customerId = Guid.NewGuid();
        customer.ID = customerId;
        customer.Name = “John Doe”;
        customer.IsActive = true;
        customer.CreatedDate = "2/6/2008 9:46:07 PM";
        customer.UpdatedDate = "2/6/2008 9:46:07 PM";
        customer.Emails = new string[2] {"John@company1.com", "Johndoe@company2.com"};
        //Validations of the Customer object member fields
        Assert.AreNotEqual(null, customer.ID, "Test Failed – Customer ID not set successfully");
        Assert.IsTrue(customer.IsActive, "Test Failed – Customer IsActive Field not set successfully");
        Assert.AreEqual(customer.Id,customerId);
        Assert.IsNotNull(customer.Id, "Test Failed – Customer Id null");
        Assert.AreNotEqual(string.Empty,customer.Name, "Test Failed – Customer Name empty");
        Assert.AreEqual(customer.Name, “John Doe”);
        Assert.AreEqual(customer.CreatedDate, “2/6/2008 9:46:07 PM”);
        Assert.AreEqual(customer.UpdatedDate, “2/6/2008 9:46:07 PM”);
        Assert.AreEqual(customer.Emails[0], "John@Company1.com");
        Assert.AreEqual(customer.Emails[1], "Johndoe@Company2.com");
    }
     
  3. Verifications should be bundled together for an entire object and implemented hierarchically, for example if a customer object has a emails collection then there should be a separate verification method for verifying email collections which should be called from the customer verification method. Consider using the new extension methods feature of .NET 3.0 (at the time of writing of this document, there is not yet enough data to prove this practice).

    Code Sample – Validations for member fields in a hierarchical manner

    Not so good: Validating all the member fields of an object in the same function

    The following code block validates all the member fields of the Customer object within the same function. This style of programming becomes unmanageable if we have several member fields and number of validations to perform on those member fields.

     [TestMethod]
    [Description("Validate Customer objects properties are set successfully")]
    public void ValidateCustomerAllFields()
    {
        //Declare and initialize a Customer object
        Customer customer = new Customer();
        Guid customerId = Guid.NewGuid();
        customer.ID = customerId;
        customer.Name = “John Doe”;
        customer.IsActive = true;
        customer.CreatedDate = "2/6/2008 9:46:07 PM";
        customer.UpdatedDate = "2/6/2008 9:46:07 PM";
        customer.Emails = new string[2] {"John@company1.com", "Johndoe@company2.com"};
        //Validations of the Customer object member fields
        Assert.AreNotEqual(null, customer.ID, "Test Failed – Customer ID not set successfully");
        Assert.IsTrue(customer.IsActive, "Test Failed – Customer IsActive Field not set successfully");
        Assert.AreEqual(customer.Id,customerId);
        Assert.AreNotEqual(string.Empty,customer.Name, "Test Failed – Customer Name empty");
        Assert.AreEqual(customer.Name, “John Doe”);
        Assert.AreEqual(customer.CreatedDate, “2/6/2008 9:46:07 PM”);
        Assert.AreEqual(customer.UpdatedDate, “2/6/2008 9:46:07 PM”);
           //Validations for Customer.Emails member field
        Assert.AreNotEqual(customer.Emails[0], string.Empty);
        Assert.AreNotEqual(customer.Emails[1], string.Empty);
        Assert.AreEqual(customer.Emails[0], "John@Company1.com");
        Assert.AreEqual(customer.Emails[1], "Johndoe@Company2.com");
    }
     

    Best practice: Using separate validations functions to validate hierarchical member fields on an object

    The following code block uses a separate verification function to validate the Emails member field of the Customer object. This style of programming is more manageable and maintainable in the long run and is more organized.

      [TestMethod]
    [Description("Validate Customer objects properties are set successfully")]
    public void ValidateCustomerAllFields()
    {
        //Declare and initialize a Customer object
        Customer customer = new Customer();
        Guid customerId = Guid.NewGuid();
        customer.ID = customerId;
        customer.Name = “John Doe”;
        customer.IsActive = true;
        customer.CreatedDate = "2/6/2008 9:46:07 PM";
        customer.UpdatedDate = "2/6/2008 9:46:07 PM";
        string[] emails = new string[2] {"John@company1.com", "Johndoe@company2.com"};
        customer.Emails = emails;
        //Validations of the Customer object member fields
        Assert.AreNotEqual(null, customer.ID, "Test Failed – Customer ID not set successfully");
        Assert.IsTrue(customer.IsActive, "Test Failed – Customer IsActive Field not set successfully");
        Assert.AreEqual(customer.Id,customerId);
        Assert.AreNotEqual(string.Empty,customer.Name, "Test Failed – Customer Name empty");
        Assert.AreEqual(customer.Name, “John Doe”);
        Assert.AreEqual(customer.CreatedDate, “2/6/2008 9:46:07 PM”);
        Assert.AreEqual(customer.UpdatedDate, “2/6/2008 9:46:07 PM”);
        //Invoke a separate validation function to validate the Emails field
        Customer.VerifyCustomerEmails(customer, emails);
    }

FakePre-46bd7622f1a74a788741d6c4c8e5ea0f-8411fd9f17c24bc1951264784bd984f8

 

 

Magic numbers and the like - Hard coded values can be a maintenance nightmare down the road. Centralize these settings somewhere in your code instead.

  1. All configuration and other data values should be in a configuration file. Initially, these can be exposed by a separate “Settings” class with static fields or methods, where the values are inline. Later, this class can be modified to get the values from a different source, like a configuration file.

    Code Sample – Use of a .config file to store commonly used values across a project

    Not so good: Hardcoding values in code is not recommended and should be avoided.

    The following code block shows use of hardcoded values in the code. Any change in the value of the connection string in the below example will cause the code to be recompiled which can prove costly if this value is used in multiple places. This practice should be avoided by storing such values in a configuration file.

    string connectionString = "Data Source= MyAppSqlServer;Initial Catalog=MyAppDatabase;
    Integrated Security=SSPI;";
    string commandString = "SaveCustomer";
    using (SqlConnection connection = new SqlConnection(connectionString))
    {
        connection.Open();
        using (SqlCommand command = new SqlCommand(commandString))
        {
            command.CommandType = CommandType.StoredProcedure;
            command.Connection = connection;
            command.Parameters.Add("@CustomerId",  SqlDbType.VarChar, 80).Value = "102903909940";
            command.Parameters.Add("@CustomerName", SqlDbType.VarChar,100).Value = "John Doe Smith";
            using (SqlDataReader reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    //Do something
                }
            }
        }
    }

    Best practice: Use a config file to store commonly used values

    The code block below demonstrates usage of a config file to store values and using those values in the code. Changes can be made to these values without the code being recompiled.

    App.Config file

      
    <add name="MyAppConnectionString" connectionString="Data Source= MyAppSqlServer;
    Initial Catalog=MyAppDatabase;Integrated Security=SSPI;"/>
    </connectionStrings>
            </configuration>
    C# code
            ConnectionStringsSection connStringSection = (ConnectionStringsSection)
    ConfigurationManager.GetSection("connectionStrings");
            string connectionString = connStringSection.ConnectionStrings["MyAppConnectionString"].ToString();
            string commandString = "SaveCustomer";
            using (SqlConnection connection = new SqlConnection(connectionString))
            {
                connection.Open();
                using (SqlCommand command = new SqlCommand(commandString))
                {
                    command.CommandType = CommandType.StoredProcedure;
                    command.Connection = connection;
                    command.Parameters.Add("@CustomerId",  SqlDbType.VarChar, 80).Value = "102903909940";
                    command.Parameters.Add("@CustomerName", SqlDbType.VarChar,100).Value = "John Doe Smith";
                    using (SqlDataReader reader = command.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            //Do something
                        }
                    }
                }
            }
  2. All strings, such as those for exception messages, etc. should be in a centrally updated Settings class.

    Code Sample – Store exception messages in a Centrally updated Settings class

    Not so good: Using exception message strings directly inline within the code while throwing exceptions

    The following code block shows exception messages strings being used inline within the code at the time of the exceptions being thrown. This practice causes issues when a exception message has to be changed and which is being used at multiple places, hence resulting in time consuming changes across the project. This should avoided by storing the exception strings within a settings class.

    /// <summary>
    /// Initialize the Customer object instance
    /// </summary>
    public Customer InitCustomer(Guid id, string name)
    {
        //Declare and initialize a CustomerV1 object
        Customer customer = new Customer();
        if (customer == null)
        {
            throw new Exception("Customer object cannot be initialized");
        }
        if (id == null)
        {
            throw new ArgumentException("Input argument 'id' passed in is set to null reference.");
        }
        if (string.IsNullOrEmpty(name))
        {
            throw new ArgumentException("Input argument 'name' passed in is Empty.");
        }
        customer.ID = id;
        customer.Name = name;
        return customer;
    }
     

    Best practice: Storing exception messages in a settings class and exposing them as  static for consumption.

    The code block below demonstrates usage of a settings class to store exception messages. This is easier to maintain and share across multiple places.

    /// <summary>
    /// Class definition for Customer object Exception messages
    /// </summary>
    public class CustomerErrorMessages
    {
        public static string InitError = "Customer object cannot be initialized";
        public static string NullParam = "Input argument 'id' passed in is set to null reference.";
        public static string EmptyParam = "Input argument 'name' passed in is Empty.";
    }
    public Customer InitCustomer(Guid id, string name)
    {
        //Declare and initialize a CustomerV1 object
        Customer customer = new Customer();
        if (customer == null)
        {
            throw new Exception(CustomerErrorMessages.InitError);
        }
        if (id == null)
        {
            throw new ArgumentException(CustomerErrorMessages.NullParam);
        }
        if (string.IsNullOrEmpty(name))
        {
            throw new ArgumentException(CustomerErrorMessages.EmptyParam);
        }
        customer.ID = id;
        customer.Name = name;
        return customer;
    }
     
  3. Settings such as web service URL's, database connection strings and assembly names/paths are another example.

    Code Sample - Sample Configuration settings

    Not so good: Using magic values in the code/hardcoding values

    The code block below is an example of hard coding values in the test code. This practice causes code change when any of these values change thus causing a maintenance nightmare and should be avoided.

     [TestMethod]
    public void SaveCustomerTest()
    {
      //Declare and initialize a Customer object
      Customer customer = new Customer();
       customer.ID = Guid.NewGuid();
       customer.Name = “John Doe”;
       customer.IsActive = true;
       CustomerService customerService= new CustomerService();
       // Hardcoding of WebService urls: Should be moved into a configuration file
       customerService.url = “http://myappserver/customerservice/customerservice.asmx”;
       customerService.Save(Customer);
       //Do Validations
    }
     

    Best practice: Store magic values outside the code in a configuration file. Use a Settings class to retrieve the values from a configuration file.

    The code block below is an example of using values from a configuration file instead of hard coding in the code itself. The settings class is a wrapper which pulls these values from the configuration and exposes them in the form of public fields.

      <connectionStrings>
        <add name="MyAppConnectionString" connectionString="Data Source= MyAppSqlServer;
    Initial Catalog=MyAppDatabase;Integrated Security=SSPI;"/>
    </connectionStrings>
    <appSettings>
    <add key="CustomerWebServiceUrl" value="http://myappserver/customerservice/customerservice.asmx" />
    <appSettings>
    //Class definition for the settings class
    public class SharedSettings
    {
        //Static constructor to load the configuration values into member fields
        static SharedSettings()
        {
            ConnectionStringsSection connStringSection = (ConnectionStringsSection)
    ConfigurationManager.GetSection("connectionStrings");
            _appConnectionString = connStringSection.ConnectionStrings["MyAppConnectionString"].ToString();
            _customerWebServiceUrl = ConfigurationManager.AppSettings["CustomerWebServiceUrl"];
        }
        //Member field to hold the connection string configuration value
        private static string _appConnectionString;
        public static string AppConnectionString
        {
            get { return SharedSettings._appConnectionString; }
            set { SharedSettings._appConnectionString = value; }
        }
        //Member field to hold the Web service url configuration value
        private static string _customerWebServiceUrl;
        public static string CustomerWebServiceUrl
        {
            get { return SharedSettings._customerWebServiceUrl; }
            set { SharedSettings._customerWebServiceUrl = value; }
        }
    }
     

    Rewriting the SaveCustomerTest method using the SharedSettings Class.

     [TestMethod]
    public void SaveCustomerTest()
    {
      //Declare and initialize a Customer object
      Customer customer = new Customer();
       customer.ID = Guid.NewGuid();
       customer.Name = “John Doe”;
       customer.IsActive = true;
       CustomerService customerService= new CustomerService();
       customerService.url = SharedSettings.CustomerWebServiceUrl;
       customerService.Save(Customer);
       //Do Validations
    }
     

     

     

     

Automation handover - Large pieces of automation are often developed by one Tester who has everything configured correctly on his or her machine. Oftentimes that Tester doesn't realize that the code cannot be compiled by another Tester who gets it from source. The cause of this is usually references to binaries that are hard-coded to a local machine path. This should be avoided using the practices below. Also, the projects should contain enough documentation that another Tester could easily figure out what to do with it. A summary document in the solution or code comments can help here significantly.

  1. All paths should use relative paths instead of absolute paths. Use Reflection to determine the execution directory (for instance GetExecutionAssembly.ToolLocation) and configuration paths. Setup & Deployment packages in VSTS sometimes require absolute paths so exceptions for this will be made but they should be clearly documented in a ReadMe file.

    Code Sample – Using Relative paths instead of absolute paths

    Not so good: Using absolute paths in code leads to runtime issues when others want to execute the same code on a different machine.

    The following code block is an example to show absolute paths being used in code. This leads to file/folder not found issues when being executed on other machines and should be avoided.

    XmlDocument xdoc = new XmlDocument();
    xdoc.Load("C:\\users\johndoe\myprojects\\xmlprojects\\testproject\\xmlfiles\test.xml");
    if (xdoc.DocumentElement.Attributes.GetNamedItem("loaded") != null)
    {
        xdoc.DocumentElement.Attributes.GetNamedItem("loaded").Value = "true";
        xdoc.Save("C:\\users\johndoe\myproject\\xmlprojects\\results\\testresults.xml");
    }
     

    Best practice: Using Relative paths in code ensures there is no dependency on which machine the code is executed

    The code block below is an example to show the use relative paths in code. This style of code ensures there are no path or folder dependencies when the code is being executed on machines other than the machine on which it was written.

    XmlDocument xdoc = new XmlDocument();
    xdoc.Load("..\\..\\xmlfiles\test.xml");
    if (xdoc.DocumentElement.Attributes.GetNamedItem("loaded") != null)
    {
        xdoc.DocumentElement.Attributes.GetNamedItem("loaded").Value = "true";
        xdoc.Save("..\\..\\results\\testresults.xml");
    }
     
  2. Each project should have a ReadMe file, this file should include any external dependencies, URL's special set-up instructions or note configuration values that need to be changed for a local machine. Testers could generate a .chm file to give an extra professional look to it, especially in the cases where an adopting tester will use it.

    Best practice: Sample Readme File for a Test Automation Project

    CustomerTest.readme File

    Product name and version number
    Customer Test Automation project, Version 1.0
    Company name
    John Doe Automation

    New and special in this release
    This Test automation project implements test automation for the Customer Application version 1.0

    Hardware and software requirements
    Pentium 4 CPU, 3.6 GHzm 1GB RAM
    Win2003, Sp1, VS.Net Framework 2.0, IIS 6.0, VS.Net 2005, Team Foundation Server Client

    Dependencies
    This automation project depends on .Net Framework 2.0 to run. Make sure its installed on the deployment machine.

    Installation instructions, getting started tips, and documentation
    Download the bits from the drop location at \\network\share\CustomerTest to C:\CustomerTest
    Open the CustomerTest.config file and change the following config entry to point to the correct Customer Database server name
    <add name="MyAppConnectionString" connectionString="Data Source= $$DBSERVER$$;Initial Catalog=MyAppDatabase;Integrated Security=SSPI;"/>
    To run the automation execute CustomerTest.exe from the command prompt.
    Look at the Results.txt file once the execution is completed to look at the test results.

    Important known problems
    Customer Test Automation projects implements test automation for Test Cases 1 thru 250.

    Version history
    None, this is the first version.

    Pricing information
    Not Applicable.

    Contact information
    Contact johndoe@johndoeautomation.com

    Date or copyright date, and other legal information   
    Feb 20, 2008, All rights reserved ©

    If extensive set-up is necessary a set-up/install document should be created with details of each step necessary.

    Code Sample – Adding Comments to Code

    Not so good: Poorly commented code

    The code block below is an example of poorly commented code. The code offers no details on what it is trying to do and why it is doing that way. 

     <connectionStrings>
        <add name="MyAppTests " connectionString="Data Source= MyAppSqlServer;
    Initial Catalog=MyAppDatabase;Integrated Security=SSPI;"/>
    </connectionStrings>
    <appSettings>
    <add key="UserId" value="11111111-1111-1111-1111-111111111111" />
    <add key="ExceptionLoggingLevel" value="0" />
    <add key="CustomerWebServiceUrl" value="http://appservername/customerservice/customer.asmx" />
    <appSettings>
     

    Best practice: Decorate your code with lots of comments. Make your code more readable and maintainable in the long run.

    The code block below demonstrates code decorated with appropriate comments. This ensures readability and is more maintainable in the long run.    

     <connectionStrings>
        <!—This setting is for Test database connection string. Test methods use this connection
    string to make connections to the Test Database to perform validations._-->
        <add name="MyAppTests" connectionString="Data Source= MyAppSqlServer;
    Initial Catalog=MyAppDatabase;Integrated Security=SSPI;"/>
    </connectionStrings>
    <appSettings>
        <!—This setting is for the Global User which Test methods use to create and save customers.
    Test methods use this setting to validate a customer as well._-->
    <add key="UserId" value="11111111-1111-1111-1111-111111111111" />
        <!—This setting is for the global product release. Test methods use it to make sure the
    appropriate version of the Customer namespace is being invoked._-->
    <add key="MyAppProductRelease" value="0" />
        <!—This setting is for the global error logging level . Test methods use it to log
    appropriate error messages in the event log. The Logging level decides the level of
    information that gets logged.-->
    <add key="ExceptionLoggingLevel" value="0" />
        <!—This setting is for the Customer Web service url. Test methods use it to talk to the
    Web service end point and make calls to the web service Web methods.-->
    <add key="CustomerWebServiceUrl" value="http://appservername/customerservice/customer.asmx" />
    <appSettings>
     

    About the Authors:

    Devin A. Rychetnik is currently working as a Software Development Engineer in Test II for the Windows Marketplace for Mobile team. In addition to testing, his 9 years of experience in software includes development, project management and security. He is currently finishing a Masters Degree in Software Development from the Open University of England and is a certified Six Sigma Green Belt and Project Management Professional (PMP).

    Syed Sohail graduated in Computer Science from Osmania University, India and is currently working as a Software Development Engineer in Test II for the Microsoft.com test team in Redmond. In addition to testing, his 11 years of experience in software includes development, project management and consulting. His main passion is around writing effective test automation and building tools which make the life of a tester easy. His team invests a lot of effort in test automation which enables them to execute regression tests and validate product code on a regular basis with minimal manual intervention.

    Other notable contributors:

    Venkat B. Iyer; Francois Burianek; Ajay Jha; Jim Lakshmipathy; Mansoor Mannan; Viet Pham; Vijaykumar Ramanujam; Sachin Joshi