如何组织这些测试以鼓励代码重用?

时间:2014-03-21 16:10:56

标签: c# unit-testing lambda organization

  

摘要

我正在组织我的单元测试,我需要一些指导来优化我的工作。

遵循单元测试的最佳实践,抽象类应通过其派生类型进行测试,从而在单元测试中构建与我的域模型中的继承层次相同​​的继承层次结构。

异常,一些测试是多余的,测试代码通过这个层次结构倍增。

例如,对属性的测试最终通过执行相同的测试并编写相同的代码行来结束。

  

您将如何组织测试?

我认为无论你进行什么测试,一些测试都是一样的。其中一些是:

  • 测试属性在分配时是否返回预期值;
  • 测试字符串属性在分配给null时是否抛出;
  • 测试在分配了比允许的字符串更长的字符串时是否抛出字符串属性;
  • 测试在超出范围值时是否抛出整数属性;
  • 测试传入null参数时是否抛出方法;
  • ...

虽然上述示例与属性相关,但对于方法以及可能想要测试的其他任何方法都采用相同的方法。

因此,这些基本测试可能属于其他测试类可以继承的SuperTestBaseClass,并从基础调用测试方法为目标测试成员。

  

一些例子

     

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;
}
  

以及其他一些基本问题

虽然这些问题可以提出一些其他好的问题,但我在这里问他们,因为我希望答案能够针对这种情况所说明的情况。

  • 如何在我的组织中使用Func<T, TResult>我想要的方式使用它?
  • 如何在我的组织中使用Action<T>我想要的方式使用它?

从这两个问题中,我希望我能让他们完全按照我作为委托参数传递的内容。

最后,

  • 您认为以这种方式组织测试是否有意?
  • 的确,SuperTestBaseClass可能属于一个类库,可供多个项目使用代码。

1 个答案:

答案 0 :(得分:0)

通过继承来重新组织测试是可行的,尽管不是类图中使用的那种继承。

[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);
        }
    }
}

这不仅鼓励代码重用,而且还以易于管理的方式在TestExplorer中组织您的测试。