The Hitchhiker's Guide To Test Driven Development

Entre Computer Services

John Betley

We're all still figuring this stuff out.

The 3 Phases

  • Resistence: Testing? Huh? Why bother?
  • Overwhelmed: What the hell do I test?
  • Evangelist: How did I get by without this?

Testing is Hard

Testing Confusion

  • Mixed best practices
  • How much to test
  • Tooling
  • Jargon. Everyone disagrees.

Acceptance, integration, functional, unit, isolated, selenium, system tests, TDD, BDD, etc.

The most accessible field in science, from the point of view of language, is astrophysics. What do you call spots on the sun? Sunspots. Regions of space you fall into and you don’t come out of? Black holes. Big red stars? Red giants.

— Neil Degrasse Tyson

Should You Really Test?

Yes!
(...and you already do)

How much is up to you, though.

Other Reasons to Test

  • Absolutely required in some cases.
  • Helps decouple code.
  • Simplifies coding. Red, Green, Refactor, Repeat.
  • Sleep better at night.

Test Driven Development

All it means is writing tests first

Why Test First?

  • Starts off with coded suite of the requirements
  • Tests are specs!
  • Forces us to think about implementation details
  • Makes tests first-class. A part of the system
The tests are a program that verifies that the system works as specified. But the system is not a program that verifies that the tests execute correctly.

— Robert "Uncle Bob" Martin

Suppose one day, all the production code disappeared.

The tests dictate how the application should function.

Pitfall:

Where Do I Begin?

Tools

  • MS Test Tools: Default, built into VS 2012.
  • Typemock/Moq: For testing in isolation.
  • Selenium: Mimic the user.

Once you've learned the basics of unit testing...

Write Acceptance Tests

Think of "acceptance tests" as tests that describe the client's expectations.
"When I visit the login page and fill in my credentials, then I should be sent to the Dashboard."

[TestMethod]
public void TestValidLoginRedirectsToDashboard()
{
    IWebDriver driver = new PhantomJSDriver();
    Selenium selenium = new WebDriverBackedSelenium(driver, "http://localhost");

    // Goto the Target website
    selenium.Open("http://localhost/login");
    
    // Fill out and submit form
    selenium.Type("username", "MyUsername");
    selenium.Type("password", "MyPassword");
    selenium.Click("submit");
    selenium.WaitForPageToLoad("1000");

    // Assert Redirect
    Assert.AreEqual("http://localhost/dashboard", driver.Url);
}
					

Be Careful!

Acceptance tests are incredibly slow.

Should I practice dependency injection?

Dependency injection, or as it's also known, passing arguments.

— Nicholas C. Zakas

Yes!


public MonthlyBilling(MemberRepositoryInterface memberRepository,
                            BillingInterface billing)
{
    this.memberRepository = memberRepository;
    this.billing = billing;
}
                    
Again, ask yourself if the given class should be responsible for a particular task. If not, get it out of there! The smaller the class, the easier it is to test.

Tip

Depend Upon Abstractions


public void DoThatThingYouDo()
{
    var chargeInfo = ['...'];

    var stripe = new StripeBilling(); // gasp!
    stripe.Charge(chargeInfo);
}

                    

// better
public MonthlyBiller(BillingInterface billing)
{
    this.billing = billing;
}

public void DoThatThingYouDo()
{
    var chargeInfo = ['...'];

    this.billing.Charge(chargeInfo); // any implementation will do
}

                    

public class StripeBilling : BillingInterface {

    public void Charge(BillingInfo info)
    {
        // ... bill with Stripe
    }

}
                    

public class BrainTreeBilling : BillingInterface {

    public void Charge(BillingInfo info)
    {
        // ... bill with BrainTree
    }

}
                    

Tip:

Mock Your Dependencies

"A mock object is nothing more than a bit of test jargon that refers to simulating the behavior of real objects."


public class MonthlyBilling {

    protected MemberRepositoryInterface memberRepository;
    protected BillingInterface billing;

    public MonthlyBilling(MemberRepositoryInterface memberRepository,
                                BillingInterface billing)
    {
        this.memberRepository = memberRepository;
        this.billing = billing;
    }

    public void Charge()
    {
        var members = this.memberRepository.GetActiveMembers();

        foreach (var member in members) {
            this.billing.Charge(member);
        }
    }
}
                    

[TestMethod]
public void TestChargesActiveMembersEachMonth()
{
    // Mock Dependencies
    var members = GetTestMembers();
    var memberRepository = Isolate.Fake.Instance<MemberRepositoryInterface>();
    Isolate.WhenCalled(() => memberRepository.GetActiveMembers()).WillReturn(members);

    var billing = Isolate.Fake.Instance<BillingInterface>();
    Isolate.WhenCalled(() => billing.Charge(null)).IgnoreCall();

    // Call function being tested
    new MonthlyBilling(memberRepository, billing).Charge();

    // Verify behavior
    int numberOfBillings = Isolate.Verify.GetTimesCalled(() => billing.Charge(null));
    Assert.AreEqual(members.Count, numberOfBillings);
    
    Isolate.Verify.WasCalledWithAnyArguments(() => memberRepository.GetActiveMembers());
}

                    

[TestMethod]
public void TestChargesActiveMembersEachMonth()
{
    var members = GetTestMembers();
    var memberRepository = MockGetActiveMembers(members);

    var billing = MockBillingCharge();

    new MonthlyBilling(memberRepository, billing).Charge();

    AssertMonthlyBillingBehavior();
}

                    

"Try not to overuse mocks. Doing so paves the way for tests that are significantly more difficult to understand and maintain. Don't forget that setting expectations on a mock allows implementation details to leak into your tests."

— Jeffrey Way

Pitfall:

What Do I Test?

Start with the business rules!

Follow Behavior Driven Development

What is BDD?

BDD is just TDD at its best!

All of your tests are written to dictate behavior

Traditional Test

  • Arrange
  • Act
  • Assert

Behavioral Test

  • Given
  • When
  • Then

Helps Make Tests Readable

Given the user does not have admin rights

When the user attempts to navigate to the admin section

Then the user is denied access


[TestMethod]
public void GivenNonAdminUser_WhenNavigatingToAdminArea_DenyAccess()
{
    // This most likely mocks out user retrieval
    GivenNonAdminUser(); 

    // var result = controller.Index() as ActionResult;
    NavigateToAdminArea();

    // Assert.AreEqual(401, controller.Response.StatusCode);
    AssertAccessDenied();
}
                    

Try it for yourself

Happy Halloween!

Thanks!

Materials, ideas, and philosophies attributed to Kent Beck, "Uncle Bob" Martin, and Jeffrey Way
Powered by reveal.js