Skip to content

pcsikos/UnitTestGenerator

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

42 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Unit Test Generator

License Build status Coverage Status NuGet

The primary purpose of Unit Test Generator is to generate repetative and boring copy/paste unit tests such as null argument check. Tests mostly validate whether an ArgumentNullException was thrown. Tests are generated by means of T4 text template. Process of the code generation consists from three parts.

  1. Method body generation
  2. Method name generation
  3. Test Class source code generation (T4)

Each part of code generation can be customized. Test method body generation is done by converting Expression tree to source code. In the process of building the body of a test method an Expression is constructed to satisfy the goal of test. This can be a method call or an object instance creation. Method body generation and test class generation (T4) should fit for most developers and unit test frameworks(changing test method attribute in T4 does not counts or hurts :). With the generation of method name it is a different case. For the test method naming was the Roy Osherove's naming strategy used. However this can be changed by your own implementation. About that later. You can find more examples of generated tests in the repository or in samples.

Generated test may look like:

     [TestMethod]
     [ExpectedException(typeof(System.ArgumentNullException))]
     public void IsValidIdentifier_VarNameNullValueGiven_ShouldThrowArgumentNullException()
     {
         var cSharpIdentifierValidator = new CSharpIdentifierValidator();		 
         cSharpIdentifierValidator.IsValidIdentifier(null);		 
     }

Getting started

PM> Install-Package UnitTestGenerator

When you have your own test generators code usage should look like this:

var assemblyTraverser = new AssemblyTraverser(typeof(MyClass).Assembly, x => true, false);
var testClassBuilder = new TestClassBuilder(assemblyTraverser, /* enumeration of test generators */);
var classes = testClassBuilder.BuildTestClasses();

Wiring up test generators with dependencies could be a skull breaker, therefore for new commers a simpler way of usage was introduced:

var testClassBuilder = typeof(CustomAssembly.Class1).Assembly
               .ComposeTestClassBuilder("CustomAssembly.Tests", configure: configure => configure
                        .IncludeBuiltinGenerators());

 foreach(var testClass in generator.BuildTestClasses())
 {
#> <--- T4 mark up
        namespace <#= TranslateNamespace(testClass.TestedType.Namespace) #>
        {
            [TestClass]
            public partial class <#= testClass.TestedType.Name #>Tests
            {                             
            ...
            ...

For the syntax above you will need to install package UnitTestGenerator.Extensions.Composition to make it work.

PM> Install-Package UnitTestGenerator.Extensions.Composition

It will install basic T4 template or you can use direct link.

Why generate tests?

Are you you asking yourself why on Earth would you generate tests at all? Yes, it is againts the idea of unit tests. Discussions about generating unit tests always tends to end with conclusions that it is a bad idea. Yes, that is true, however this is not that case. Unit Test Generator only analyzes the signature of methods not their bodies and regarding that generates test methods to validate whether null arguments are handled properly. It does exactly that, what developer would do when they would write argument validation tests. Benefit from Unit Test Generator is time saving(each nullable parameter leads to another test) and it avoids human errors when tests are manually written (forgeting writing test for new methods, argument checks for new parameters in existing methods, etc.).

When generate tests?

Most suitable scenario for generating tests is Domain Driven Design when you need test arguments of methods and constructors as well for null reference. Service classes needs mostly be tested for null refenrence only in methods, because constructors are handled by IoC containers and thus do not need argument validation. At least not for null reference.

Advanced Scenarios

##Custom method name generation When you need only change a part of the generation, for example the method name generation, you do not need create classes for everything else.

	class MyCustomCodeGenerator : NullArgumentMethodTestMethodSourceCodeGenerator
    {
        public MyCustomCodeGenerator(IExpressionBuilder expressionBuilder, 
            ITestMethodValueProvider testMethodValueProvider) : base(expressionBuilder, testMethodValueProvider)
        {
        }

        public override string BuildMethodName(MethodSourceCodeGenerationRequest request)
        {
            return request.Method.Name + "_ShouldAlwaysSucceed";
        }
    }

Configuring the generator is then pretty easy:

var testClasses = typeof(CustomAssembly.Class1).Assembly
               .ComposeTestClassBuilder("CustomAssembly.Tests", 
	               container =>
                    container.Register<INullArgumentMethodTestMethodSourceCodeGenerator, MyCustomCodeGenerator2>(),
	               configure => configure.AddGenerator<CustomTestMethodGenerator>())
               .BuildTestClasses();

Check the sample.

##Custom source code and method name generation

Most advanced customization is your own source code generation. This can be done by inheriting from existing Code Generator like NullArgumentMethodTestMethodSourceCodeGenerator.

class MyCustomCodeGenerator : NullArgumentMethodTestMethodSourceCodeGenerator
    {
        public MyCustomCodeGenerator(IExpressionBuilder expressionBuilder, 
            ITestMethodValueProvider testMethodValueProvider) : base(expressionBuilder, testMethodValueProvider)
        {
        }

        protected override void BuildArrangeSourceCode(MethodSourceCodeGenerationRequest request)
        {
            AppendLine("//TODO: Arrange");
        }

        protected override void BuildActSourceCode(MethodSourceCodeGenerationRequest request)
        {
            AppendLine("//TODO: Act");
        }

        protected override void BuildAssertSourceCode(MethodSourceCodeGenerationRequest request)
        {
            AppendLine("//TODO: Assert");
        }
    }

Or by implementing your own ITestMethodSourceCodeGenerator.

class MyCustomCodeGenerator2 : ITestMethodSourceCodeGenerator<MethodSourceCodeGenerationRequest>
    {
        public string BuildMethodName(MethodSourceCodeGenerationRequest request)
        {
            return request.Method.Name + "_ShouldAlwaysSucceed";
        }

        public string BuildSourceCode(MethodSourceCodeGenerationRequest request)
        {
            var source = @"
//TODO: Arrange
//TODO: Act
//TODO: Assert
";
            return source;
        }
    }

Do not forget. Generation of source code is just a part of the job. You must also create class which will analyze the passed type whether it can be used or on what members.

    public class CustomTestMethodGenerator : ITestMethodGenerator
    {
        public IEnumerable<TestMethod> GenerateTestMethods(TypeContext typeContext)
        {
            var sourceCodeGenerator = new MyCustomCodeGenerator2();
            foreach (var property in typeContext.TargetType.GetProperties())
            {
                var request = new MethodSourceCodeGenerationRequest(property);
                var methodName = sourceCodeGenerator.BuildMethodName(request);
                var sourceCode = sourceCodeGenerator.BuildSourceCode(request);
                yield return new TestMethod(property, methodName, sourceCode);
            }
        }
    }

Check the working sample.

var testClassBuilder = typeof(CustomAssembly.Class1).Assembly
               .ComposeTestClassBuilder("CustomAssembly.Tests", configure: configure => configure.AddGenerator<CustomTestMethodGenerator>())
               .BuildTestClasses();

##Configuring ClassBuilder composition You can customize configuration using UnitTestGenerator.Extensions.Composition.

###Reusing tested instance In a case you have an instance of tested class as a member field in you own test, you can reuse it in generated tests. Your hand written test may look like this:

	[TestClass]
    public partial class MyClassTests
    {
	    private MyClass myClassInstance;
		...
		//your test methods
		...
    }

And to reuse the myClassInstance you can register the name of the field member:

var testClassBuilder = typeof(CustomAssembly.Class1).Assembly
               .ComposeTestClassBuilder("CustomAssembly.Tests", configure: configure => configure.IncludeBuiltinGenerators()
                  .ParameterTypeMapping(new Dictionary<Type, string> {
                             { typeof(MyClass), "myClassInstance"  } 
                        }))
              .BuildTestClasses();

Do not forget that the generated class must be partial and must be placed in the same namespace as your own to be visible from generated class. Check sample.

###Defining default values To test arguments for null values, you need specify the tested argument as null and the remaining parameters with valid non null value. Therefore the other fields are proxied or dynamically generated at the execution of test. However, you may have classes which cannot be instantiated with any of the mentioned ways. For example the class MemberInfo cannot be instantiated nor proxied. To solve this issue Expression registration was introduced, which replace dynamic Expression generation which is used to source code generation. Therefore the registered expression will be converted to source code as well. Check sample.

var testClassBuilder = typeof(CustomAssembly.Class1).Assembly
               .ComposeTestClassBuilder("CustomAssembly.Tests", configure: configure => configure.IncludeBuiltinGenerators()
					.WithDefaultValues(new[] {
                         (Expression<Func<MemberInfo>>)(() => ((Func<string, string>)string.Copy).Method)
                        }))
              .BuildTestClasses();

Issues

It is higly likely that not all code generation issues were handled. Issue reports are extremely usefull.

Reviews, pull requests, forks and stars are welcome :)

About

Extensible source code generator of simple unit tests

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published