Smart Tests

Discover what Smart Tests is:

Press space to go to the next slide.

It is best viewed with latest browsers (do not use Internet Explorer please!).

Context

If you are not yet convinced of the Advantages of Unit Testing!

Originaly, I developed Visual T#, an enhanced Visual C# for Unit Testing.

It did not succeeed as expected. Problems were:

Recently, I decided to try to port as much as possible Visual T# ideas in a library, instead of a different language: Smart Tests: What's that?

Advantages of Unit Testing

Unit Tests, when used correctly, enable you to have

Visual T#

Visual T# is a programming language I developed... in a previous life!

It was based on C# (2.0 at that time) with Keywords added to simplify Unit Tests coding.

Visual T# was developed using C#, tested with NUnit and... Visual T# itself... (About 30.000 Unit Tests)

What is Smart Tests?

As it was difficult for pepole to switch from C# to T#, I decided to put as much T# features as possible in:

For the library part, Smart Tests has the same features as Visual T#:

For The Visual Studio Analyzer part:

But, unlike Visual T#, you keep your favorite language: C# and your favorite Testing Framework: NUnit, MSTests...

Smart Tests

We will explore Smart Tests from different questions:

What a 'Unit' test should be?

We want to write Unit Tests, but what is a Unit?

Very different responses exist, depending on the people you ask:

  • A Method
  • A Method Call for a specific context
  • A Side Effect of a Method Call for a specific context

Problem is that when a test fails somewhere, you do not know what is wrong after the failing line of code!

If your tests are at a high level:

  • It will contain too much validations
  • You will not know the extent of damage

If your tests are at a low level:

  • It will contain too much replications from tests to tests
  • You will have a very long maintenance

Here is what a 'Unit' test should be!

Three (four) parts: 3A (4A)

  • Arrange: to prepare anything needed by the Act part
  • (Assume): to validate the context of the Act part
  • Act to test whatever you need to test
  • Assert: to verify that any side effects of the Act part are those expected

Important: The Act should be 1 line of code only; for the best balance between knowledge and maintenance

Thus, the response is: a Method Call for a specific context.

Where is the Act?

The Act is the most important part of your test

You write the test to run this line of code.

The Act is the second/third part of three/four parts...

var p1 = new Person("Ludovic", "Dubois");
var p2 = new Person("FirstName", "LastName");
var a1 = new Account(p1, 100);
var a2 = new Account(p2, 200);
var t = new Transfer(a2, a1, 50);
var ts1 = a1.Transactions;
var ts2 = a2.Transactions;
var t1 = ts1[ts1.Count - 1];
var t2 = ts2[ts2.Count - 1];
Assert.AreEqual(t1, t);
Assert.AreEqual(t2, t);
Assert.AreEqual(150, a1.Balance);
Assert.AreEqual(150, a2.Balance);

Where is the Act here?

Here is the Act!

Where is the Act now?

var p1 = new Person("Ludovic", "Dubois");
var p2 = new Person("You", "Ubisoft");
var a1 = new Account(p1, 100);
var a2 = new Account(p2, 200);
var t = RunTest( ..., () => new Transfer(a2, a1, 50));
var ts1 = a1.Transactions;
var ts2 = a2.Transactions;
var t1 = ts1[ts1.Count - 1];
var t2 = ts2[ts2.Count - 1];
Assert.AreEqual(t1, t);
Assert.AreEqual(t2, t);
Assert.AreEqual(150, a1.Balance);
Assert.AreEqual(150, a2.Balance);

As you can see, the RunTest method call enables you to easily retrieve the most important line of your test.

Thus, everything that is before RunTest is for the Arrange/Assume parts and everything that is after the RunTest is for the Assert part.

Which Unit Tests are missing?

To know what unit tests are missing, you can use Code Coverage Tools.

But it is no sufficient:

  • Accuracy depends on the technology of the tool
    • Function coverage
    • Statement coverage
    • Branch coverage
    • Condition coverage
    • ...
  • Not sufficient, whatever technology you use

For example, here is a test:

// Arrange
var p = new Person("Ludovic", "Dubois");

// Act
p.ClientType = ClientType.VIP;

// Assert
Assert.AreEqual(ClientType.VIP, p.ClientType);

Which tests are missing?

Here are the missing Unit Tests!

Smart Tests defines Criteria

A criteria declares several criterions:

For example, ValidValue criteria has 2 criterions:

  • IsValid
  • IsInvalid

Thus, as the first argument of your RunTest call, specify a criterion for your intent:

var p = new Person("Ludovic", "Dubois");

RunTest( ValidValue.IsValid,
         Assign( () => p.ClientType = ClientType.VIP ) );

Assert.AreEqual(ClientType.VIP, p.ClientType);

When saving your test file, a warning indicates that ValidValue.IsInvalid is not yet tested for Person.ClientType setter!

Thanks to the Visual Studio Analyzer.

Smart Tests defines lots of Criteria, and you can define your own.

Which Unit Tests are missing?

A more complex example

// Arrange
var p1 = new Person("Ludovic", "Dubois");
var p2 = new Person("You", "Ubisoft");
var a1 = new Account(p1, 100);
var a2 = new Account(p2, 200);

// Act
var t = new Transfer(a2, a1, 50);

// Assert
var ts1 = a1.Transactions;
var ts2 = a2.Transactions;
var t1 = ts1[ts1.Count - 1];
var t2 = ts2[ts2.Count - 1];
Assert.AreEqual(t1, t);
Assert.AreEqual(t2, t);
Assert.AreEqual(150, a1.Balance);
Assert.AreEqual(150, a2.Balance);

Which tests are missing?

Here are the missing Unit Tests!

Add criteria per parameter

var p1 = new Person("Ludovic", "Dubois");
var p2 = new Person("You", "Ubisoft");
var a1 = new Account(p1, 100);
var a2 = new Account(p2, 200);

var t = RunTest( Case("fromAccount", ValidValue.IsValid) &&
                 Case("toAccount", ValidValue.IsValid) &&
                 Case((double amount) => amount.Above(0, out var value),
                 () => new Transfer(a2, a1, value));

var ts1 = a1.Transactions;
var ts2 = a2.Transactions;
var t1 = ts1[ts1.Count - 1];
var t2 = ts2[ts2.Count - 1];
Assert.AreEqual(t1, t);
Assert.AreEqual(t2, t);
Assert.AreEqual(100 + value, a1.Balance);
Assert.AreEqual(200 - value, a2.Balance);

Look at available criteria and their usage here

Smart Assertions

Smart Assertions are high level assertions

With implications on Arrange, Assume and/or Assert parts of your tests.

You will learn:

How to test an object did not changed?

Testing an object property changed as expected is good, but testing no property changed is good too!

How do you do that?

No secret: take a backup of all the properties before, and compare them with the values after!

// Arrange
var p = new Person("Ludovic", "Dubois");
var fn = p.FirstName;
var ln = p.LastName;
var ct = p.ClientType;
...

// Act
var se = p.SendEmail(...);

// Assert
Assert.IsTrue(se);
Assert.AreEqual(fn, p.FirstName);
Assert.AreEqual(ln, p.LastName);
Assert.AreEqual(ct, p.ClientType);
...

Here is how to test an object did not changed!

Use Smart Assertion NotChanged

var p = new Person("Ludovic", "Dubois");

var se = RunTest(...,
                 () => p.SendEmail(...),
                 SmartAssert.NotChanged() );

Assert.IsTrue(se);

Here is what this assertion do:

  • Before Act
    Creates a backup of properties and/or fields.
  • After Act
    Ensures that the properties and/or fields have the previous values.

How to test an object did not changed?

A more complex version

// Arrange
var p = new Person("Ludovic", "Dubois");
var a = new Account(p1, 100);
var fn = p.FirstName;
var ln = p.LastName;
...

// Act
var t = new Deposit(a, 50);

// Assert
var d = a.Transactions[a.Transactions.Count - 1];
Assert.AreEqual(t, d);
Assert.AreEqual(150, a.Balance);
Assert.AreSame(p, a.Owner);
Assert.AreEqual(fn, p.FirstName);
Assert.AreEqual(ln, p.LastName);
...

Here is how to test an object did not changed!

Use Smart Assertion NotChanged

var p = new Person("Ludovic", "Dubois");
var a = new Account(p1, 100);

var t = RunTest(...,
                () => new Deposit(a, 50),
                SmartAssert.NotChanged(p) );

var d = a.Transactions[a.Transactions.Count - 1];
Assert.AreEqual(t, d);
Assert.AreEqual(150, a.Balance);
Assert.AreSame(p, a.Owner);

How to test your test is meaningful?

Testing that property has the right value in the Assert part is good, but testing that this property did not have this value previously is far better.

How do you do that?

No secret: Ensure the value is not the final one in the Assume part

// Arrange
var p = new Person("Ludovic", "Dubois");

// Assume
Assert.AreNotEqual(ClientType.VIP, p.ClientType);

// Act
p.ClientType = ClientType.VIP;

// Assert
Assert.AreEqual(ClientType.VIP, p.ClientType);

Here is how to test your test is meaningful!

Use Smart Assertion ChangedTo

var p = new Person("Ludovic", "Dubois");

RunTest(..., 
        Assign(() => p.ClientType, ClientType.VIP),
        SmartAssert.ChangedTo());

Here is what this assertion do:

  • Before Act
    Ensures that the property value is not the one to be assigned in the Act.
  • After Act
    Ensures that the property value is the one to be assigned in the Act.

How to test your test is meaningful?

A more complex version

// Arrange
var p = new Person("Ludovic", "Dubois");
var a = new Account(p1, 100);

// Assume
Assert.AreNotEqual(150, a.Balance);

// Act
var t = new Deposit(a, 50);

// Assert
var d = a.Transactions[a.Transactions.Count - 1];
Assert.AreEqual(t, d);
Assert.AreEqual(150, a.Balance);

Here is how to test your test is meaningful!

Use Smart Assertion ChangedTo

var p = new Person("Ludovic", "Dubois");
var a = new Account(p1, 100);

var t = RunTest(..., 
                () => new Deposit(a, 50),
                SmartAssert.ChangedTo(() => a.Balance, 150));

var d = a.Transactions[a.Transactions.Count - 1];
Assert.AreEqual(t, d);

How to test an object changed as expected?

Testing an object property changed as expected is good, but testing no other property changed is good too!

How do you do that?

No secret: take a backup of all the properties (except the one that should change) before, and compare them with the values after!

// Arrange
var p = new Person("Ludovic", "Dubois");
var fn = p.FirstName;
var ln = p.LastName;
...

// Assume
Assert.AreNotEqual(ClientType.VIP, p.ClientType);

// Act
p.ClientType = ClientType.VIP;

// Assert
Assert.AreEqual(ClientType.VIP, p.ClientType);
Assert.AreEqual(fn, p.FirstName);
Assert.AreEqual(ln, p.LastName);
...

Here is how to test an object did not changed!

Use Smart Assertion NotChangedExcept

var p = new Person("Ludovic", "Dubois");

RunTest(..., 
        Assign(() => p.ClientType, ClientType.VIP),
        SmartAssert.NotChangedExcept(),
        SmartAssert.ChangedTo()); // test more!

Here is what this assertion do:

  • Before Act
    Ensures that all exceptions are properties/fields of the instance involved in the Act.
    Creates a backup of properties and/or fields.
  • After Act
    Ensures that the properties and/or fields have the values of the backup.

How to test an object changed as expected?

A more complex version

// Arrange
var p = new Person("Ludovic", "Dubois");
var a = new Account(p1, 100);
var pid = a.Id;
var pCD = a.CreationDate;
...

// Act
var t = new Deposit(a, 50);

// Assert
var d = a.Transactions[a.Transactions.Count - 1];
Assert.AreEqual(t, d);
Assert.AreEqual(150, a.Balance);
Assert.AreEqual(pId, a.Id);
Assert.AreEqual(pCD, a.CreationDate);
Assert.AreSame(p, a.Owner);
...

Here is how to test an object changed as expected!

Use Smart Assertion NotChangedExcept

var p = new Person("Ludovic", "Dubois");
var a = new Account(p1, 100);

var t = RunTest(..., 
               () => new Deposit(a, 50),
               SmartAssert.NotChangedExcept(a, nameof(Account.Balance)),
               SmartAssert.ChangedTo(() => a.Balance, 150) ); // test more!

var d = a.Transactions[a.Transactions.Count - 1];
Assert.AreEqual(t, d);

How to test PropertyChanged is raised?

Testing an object property changed as expected is good, but testing that PropertyChanged event is raised is good too!

How do you do that?

No secret:

  • In the Arrange part
    • Initialize a boolean field with false.
    • Subscribe to the PropertyChanged event.
  • In the Act part
    • Set the boolean field to true.
    • Assert that the args.PropertyName is the expected one.
    • Assert that the property value is the expected one.
  • In the Assert part
    • Unsubscribe from the PropertyChanged event.
    • Check the boolean value field is now true.
// Arrange
var p = new Person("Ludovic", "Dubois");
pcRaised = False;
p.PropertyChanged += PC;

// Act
p.ClientType = ClientType.VIP;

// Assert
p.PropertyChanged -= PC;
Assert.IsTrue(pcRaised);
Assert.AreEqual(ClientType.VIP, p.ClientType);
// In PC method
pcRaised = true
Assert.AreEqual(nameof(Person.ClientType), args.PropertyName);
Assert.AreEqual(ClientType.VIP, p.ClientType);

Here is how to test PropertyChanged is raised!

Use Smart Assertion Raised_PropertyChanged

var p = new Person("Ludovic", "Dubois");

RunTest(..., 
        Assign(() => p.ClientType, ClientType.VIP),
        SmartAssert.Raised_PropertyChanged(),
        SmartAssert.NotChangedExcept(), // test more!
        SmartAssert.ChangedTo()); // test more!

How to test PropertyChanged is raised?

A more complex version

// Arrange
var p = new Person("Ludovic", "Dubois");
var a = new Account(p1, 100);
pcRaised = False;
a.PropertyChanged += PC;

// Assume
Assert.AreNotEqual(150, a.Balance);

// Act
var t = new Deposit(a, 50);

// Assert
a.PropertyChanged -= PC;
Assert.IsTrue(pcRaised);
var d = a.Transactions[a.Transactions.Count - 1];
Assert.AreEqual(t, d);
Assert.AreEqual(150, a.Balance);
// In PC method
pcRaised = true
Assert.AreEqual(nameof(Account.Balance), args.PropertyName);
Assert.AreEqual(150, ((Account)sender).Balance);

Here is how to test PropertyChanged is raised!

Use Smart Assertion Raised_PropertyChanged

var p = new Person("Ludovic", "Dubois");
var a = new Account(p1, 100);

var t = RunTest(...,
                () => new Deposit(a, 50),
                SmartAssert.Raised_PropertyChanged(() => a.Balance, 150),
                SmartAssert.ChangedTo(() => a.Balance, 150),
                SmartAssert.NotChangedExcept(() => a.Balance)); // test more!

var d = a.Transactions[a.Transactions.Count - 1];
Assert.AreEqual(t, d);

How to test PropertyChanged is not raised?

Testing an object property changed as expected is good, but testing that PropertyChanged event is not raised is good too!

How do you do that?

No secret:

  • In the Arrange part
    • Initialize a boolean field with false.
    • Subscribe to the PropertyChanged event.
  • In the Registered Handler
    • Set the boolean field to true.
  • In the Assert part
    • Unsubscribe from the PropertyChanged event.
    • Check the boolean value field is now false.
// Arrange
var p = new Person("Ludovic", "Dubois");
pcRaised = False;
p.PropertyChanged += PC;

// Act
p.SendEmail(...);

// Assert
p.PropertyChanged -= PC;
Assert.IsFalse(pcRaised);
// In PC method
pcRaised = true

Here is how to test PropertyChanged is not raised!

Use Smart Assertion Not_Raised_PropertyChanged

var p = new Person("Ludovic", "Dubois");

RunTest(..., 
        () => p.SendEmail(...),
        SmartAssert.NotRaised_PropertyChanged(),
        SmartAssert.NotChanged()); // test more!

How to test PropertyChanged is not raised?

A more complex version

// Arrange
var p = new Person("Ludovic", "Dubois");
var a = new Account(p1, 100);
pcRaised = False;
p.PropertyChanged += PC;

// Assume
Assert.AreNotEqual(150, a.Balance);

// Act
var t = new Deposit(a, 50);

// Assert
p.PropertyChanged -= PC;
Assert.IsFalse(pcRaised);
var d = a.Transactions[a.Transactions.Count - 1];
Assert.AreEqual(t, d);
Assert.AreEqual(150, a.Balance);
// In PC method
pcRaised = true;

Here is how to test PropertyChanged is not raised!

Use Smart Assertion Not_Raised_PropertyChanged

var p = new Person("Ludovic", "Dubois");
var a = new Account(p1, 100);

var t = RunTest(..., 
                () => new Deposit(a, 50),
                SmartAssert.ChangedTo(() => a.Balance, 150),
                SmartAssert.NotRaised_PropertyChanged(p));
 
var d = a.Transactions[a.Transactions.Count - 1];
Assert.AreEqual(t, d);

How to test any event is not raised?

Testing an object property changed as expected is good, but testing any event is good too!

How do you do that?

No secret: quite the same as PropertyChanged, except that the check in the event handler do not test the PropertyName, but anything else.

Here is how to test an event is raised!

Use Smart Assertion Raise/NotRaised.

Why testing with hard-coded values?

Hard-coded values do not show logic of your Act part

// Arrange
var p = new Person("Ludovic", "Dubois");
var a = new Account(p1, 100);

// Act
var t = new Deposit(a, 50);

// Assert
var ts = a.Transactions;
Assert.AreEqual(1, ts.Count); // Is it always one? Always from 0 to 1? Or one more than before?
Assert.AreEqual(ts[0], t);
Assert.AreEqual(150, a.Balance);

Why not testing with relative values?

Use Smart Assertion Change

var p = new Person("Ludovic", "Dubois");
var a = new Account(p1, 100);

var t = RunTest(...,
                () => new Deposit(a, 50),
                SmartAssert.Change(() => a.Transactions.Count + 1)); // It is one more than before!

var ts = a.Transactions;
Assert.AreEqual(ts[ts.Count - 1], t);
Assert.AreEqual(150, a.Balance);

Here is what this assertion do:

  • Before Act
    Evaluates the expression and keep its result.
  • After Act
    Strip the expression of any hard-coded compuation (+1 in this case) and
    ensures it is the same value as the previous one.

How to ensure Act is not too long?

You have to start a StopWatch before your Act and stop it after your Act.

// Arrange
var p = new Person("Ludovic", "Dubois");
var watch = new StopWatch();

watch.Start();
// Act
p.SendEmail(...);

// Assert
watch.Stop();
Assert.Less(watch.TotalMilliseconds, 100);

Here is how to ensure Act is not too long!

Use Smart Assertion Within

var p = new Person("Ludovic", "Dubois");

RunTest(..., 
        () => p.SendEmail(...),
        SmartAssert.Within(100));

Here is what this assertion do:

  • Before Act
    Start a StopWatch.
  • After Act
    Stop the StopWatch and verifies that the elapsed milliseconds is less than the specified value.

How to test asynchronous calls?

You have to wait for a handle hat is set within the asynchronous call.

// Arrange
var p = new Person("Ludovic", "Dubois");
var evt = new AutoResetEvent(false);

// Act
p.SendEmail(..., sent:() => evt.Set());

// Assert
Assert.IsTrue(evt.WaitOne(1000));

Here is how to test asynchronous calls!

Use Smart Assertion Within

var p = new Person("Ludovic", "Dubois");

RunTest(..., 
        ctx => p.SendEmail(..., done:ctx.SetHandle),
        SmartAssert.WaitContextHandle(1000));

Here is what this assertion do:

  • Before Act
    Create an AutoResetEvent.
  • After Act
    Wait for the AutoResetEvent for the specified time.

Conclusion

If you are used to write Unit Tests, you surely understand what Smart Tests can bring to you and your team!

Do not forget that you combine any assertions you just learn!

For more information: