Question

Summary

I am working at organizing my unit tests, and I need some guidance to optimize my efforts.

Following the best practices of unit testing, an abstract class shall get tested through its derived types, thus building about the same inheritance hierarchy within my unit tests than the one in my domain model.

Exception made, some tests are redundant and the test code is multiplied though this hierarchy.

For instance, the testing of properties ends up by doing about always the same tests and writing the same lines of code.

How would you organize your tests?

I think that some tests are the same whatever you're testing. Some of them are:

  • Test whether a property returns the expected value upon assignment;
  • Test whether a string property throws when assigned to null;
  • Test whether a string property throws when assigned a longer string than permitted;
  • Test whether an integer property throws when out of range values is assigned;
  • Test whether a method throws when a null argument is passed in;
  • ...

Though the above-mentioned examples are property-related, take the same for methods and whatsoever else one could want to test.

So, those basic tests could belong to a SuperTestBaseClass from which the other test classes could inherit from, and call the test methods from the base for the targeted tested members.

Some examples

AuditableEntity

public abstract class AuditableEntity {
    protected AuditableEntity() { }

    DateTime CreatedAt { get; set; }
    string CreatedBy { get; set; }
    DateTime DeletedAt { get; set; }
    string DeletedBy { get; set; }
    int Id { get; protected set; }
    DateTime UpdatedAt { get; set; }
    string UpdatedBy { get; set; }
}

Customer

public class Customer : AuditableEntity {
    public class Customer() : base() { Invoices = new Collection<Invoice>(); }

    public string Name { get; set; }
    public IEnumerable<Invoice> Invoices { get; private set; }
    public long PhoneNumber { get; set; }
}

Invoice

public class Invoice : AuditableEntity {
    public class Invoice() : base() { Items = new Collection<Item>(); }

    public IEnumerable Items { get; private set; }
    public double GrandTotal { get { return Items.Sum<Item>(i => i.Price); } }
}

SuperTestBaseClass

public abstract class SuperTestBaseClass {
    protected SuperTestBaseClass() { }

    protected void Throws<TException>(Action<T> action) {
        // arrange
        Type expected = typeof(TException);
        Exception actual = null;

        // act
        try { action; } catch (Exception ex) { actual = ex; }

        // assert
        Assert.IsInstanceOfType(actual, expected);
    }

    protected void PropertyGetSetValue(Action<T> action, T value) {
        // arrange
        T expected = value;
        action; // assign the value to the property, let's say

        // act
        T actual = action; // gets the value out of the property, let's say

        // assert
        Assert.AreEqual(expected, actual);
    }
}

AuditableEntityTests<T>

public abstract class AuditableEntityTests<T> where T : IAuditableEntity : SuperTestBaseClass {
    [TestMethod]
    public CreatedAt_ReturnsNowByDefault() {
        // arrange
        DateTime expected = DateTime.Now;

        // act
        DateTime actual = Entity.CreatedAt;

        // assert
        Assert.AreEqual(expected, actual);  
    }        

    [TestMethod]
    public void CreatedBy_ThrowsArgumentNullExceptionWhenNullOrWhiteSpace() {            
        Throws<ArgumentNullException>(Entity.CreatedBy = null);
    }

    protected T Entity { get; set; }
}

CustomerTests

[TestClass]
public class CustomerTests : AuditableEntityTests<Customer> {
    public CustomerTests() : base() { }

    [TestMethod]
    public void Name_GetSetValue() { 
        PropertyGetSetValue(customer.Name, RandomValues.RandomString());
    }

    [TestMethod]
    public void Name_CannotBeNull() {
        Throws<ArgumentNullException>(Customer.Name = null);
    }

    [ClassInitialize]
    public void CustomerEntitySetUp() { Entity = customer; }

    [TestInitialize]
    public void CustomerSetUp() { customer = new Customer(); }

    private Customer customer;
}

And a few other underlying questions

Althought these question could make some other good questions, I ask them here since I want the responses to be oriented to the situation illustrated by this very context.

  • How to use Func<T, TResult> the way I want to use it in my organization?
  • How to use Action<T> the way I want to use it in my organization?

From these two questions, I wish I could just make them do exactly what I passed in parameter as delegate.

And finally,

  • Do you think it is worht to organize the tests this way?
  • Indeed, the SuperTestBaseClass could belong to a class library for code use through multiple projects.
Was it helpful?

Solution

Reorganizing your tests is doable through inheritance, though not the same kind of inheritance used in class diagrams.

[TestClass]
public abstract class AuditableEntityTests {
    protected AuditableEntityTests(IAuditableEntity entity) { Entity = entity; }
    protected IAuditableEntity Entity { get; set; }

    public abstract void GetsAndSetsValue();
    public virtual Throws<TException>(Action action) { 
        // arrange
        Type expected = typeof(TException);
        Exception actual = null;

        // act
        try { action(); } catch(Exception ex) { actual = ex; }

        // assert
        Assert.IsInstanceOfType(actual, expected);
    }

    [TestClass]
    public class CreatedAt : AuditableEntityTests {
        public CreatedAt() : base(new Customer()) { }

        [TestMethod]
        public void GetsAndSetsValue() {
            // arrange
            DateTime expected = DateTime.Now;
            Entity.CreatedAt = expected;

            // act
            DateTime actual = Entity.CreatedAt;

            // assert
            Assert.AreEqual(expected, actual);
        }
    }

    [TestClass]
    public class CreatedBy : AutditableEntityTests {
        public CreatedBy() : base(new Customer());

        [TestMethod]
        public void GetsAndSetsValue() {
            // arrange
            string expected = RandomValues.RandomString();
            Entity.CreatedBy = expected;

            // act
            string actual = Entity.CreatedBy;

            // assert
            Assert.AreEqual(expected, actual);
        }

        [TestMethod]
        public void CannotBeNull() {
            // arrange 
            string unexpected = null;
            Entity.CreatedBy = RandomValues.RandomString();

            // act
            Entity.CreatedBy = unexpected;
            string actual = Entity.CreatedBy;

            // assert
            Assert.IsNotNull(actual);
        }

        [TestMethod]
        public void ThrowsWhenLongerThan12() {
            // arrange
            int length = 256;
            string tooLong = RandomValues.RandomString(length);

            // act
            Action action = () => { Entity.CreatedBy = tooLong; };

            // assert
            Throws<ArgumentOutOfRangeException>(action);
        }
    }
}

Not only does this encourage code reuse, but it also organize your tests in the TestExplorer in an easy to manage way.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top