I know I should be Unit Testing, but I don't know how or where to start - presentation version

October 17, 2018    Development UnitTesting DevOps BDD AutomatedTesting CodeCamp NECode Presentation

I know I should be Unit Testing, but I don’t know how or where to start - Presentation Version

I blogged about this earlier in the year , but this article will expand on that by re-organizing to give information for my presentation at SD Code Camp 2018 .

I’ve shared my PowerPoint for SD Code Camp 2018 which has a lot of the same information.

Here’s a slightly updated version for NE Code .

The NE Code intro

How would you like to do less manual testing, reduce fear: FDD and fear of deployment, stop fixing the same bugs over and over, deploy more frequently, force clarity of business rules, document assumptions, help others on board more quickly, increase business success, reduce stress, improve your life and the lives of your team (dev, ops, QA, managers, execs) and improve overall happiness?​​

Does that sound impossible and utopian? Maybe, but I’ve seen the promised land (at least parts of it). I have also been stuck wandering the wilderness of bug regressions, wasted time, energy and lost opportunities. ​​

The journey to the promise land can be long (hopefully it won’t take you 40 years), is made up of many steps (crawl, walk, run) and it won’t be easy, but is worth the effort. ​​

I am convinced that automated testing is one of the cornerstones (*) to successful software projects and DevOps. This is backed by research aka Accelerate and experience. ​​

My goal is that by the end of the hour you’ll be motivated to increase learning with your team and start or continue adding more automated tests to verify your systems. I hope you’ll go back to work on Monday thinking “I can and should do this!”​​

Let’s talk more about automated testing.​​

  • (along with understanding business needs and Clean Architecture.)​

(I added the intro for NE Code 2019 to spice it up. I originally started with my intro slide and the Goal section below).

Goal

That you’ll leave with an idea of how to get unit testing started in your current or upcoming projects. You’ll be able to go back to work on Monday, try unit testing and start learning, thinking “I can do this!”.

I hope to help you get started moving past not knowing how and where to start and make automated testing as normal to you as writing code.

The hope is that this will lead to higher quality code, more confidence in refactoring, easier on boarding for teammates, less stressful and more often releases and better developer lives.

Notes about the Github project

My code is on Github for you to look at. There are notes in the README.md about the project.

Talk outline from the PowerPoint

Intro

Terms

  • Seams (faking calls to a dependency)
  • Could be HTTP, FileSystem, DateTime, other dependencies you’ve created
  • API – something that responds to HTTP requests
  • Fake – returning dummy data
  • Mock – verifying
  • Dependency Injection – passing a class into a class, usually through the constructor

Testing Pyramid

Testing Pyramid => I’m focused on the unit testing part

Follow the Testing Pyramid .

Three Main Camps

  1. I don’t need to write tests, I do just fine without them
  2. I know it can be helpful, but….
  3. I write tests and see it as a part of my every day work, but can get better

A Journey, My Experiences

  • 2004 College TDD, xP programming
  • 2006 – first job, WebForms, no unit testing
  • 2008 – I first read the Art of Unit Testing and tried to apply in Silverlight MVVM.
  • 2012-2013 – new project, MVC, KnockoutJS, Jasmine testing   - no build system, the tests fell out of use and usefulness as the team grew
  • 2013 to 2018 – UI tests > Jasmine KnockoutJS tests (some MVC) + UI tests by developers   - TFS on premise builds to CI, run UI tests, 5000+ TypeScript tests, some MVC tests   - normal part of my day now
  • 2018 to 2019 - Working on a (building on top of 30 years of systems) project that is now several years old without tests, lacking separation of concerns in the code and loads of regression issues has been a challenge.

See the more in-depth version of this .

Lessons Learned

  • need a build to keep tests passing
  • It takes time to learn and do (ex a 5 minute change may turn into a few hours in creating the seams
  • It’s an art, not a science. Many different ways.
  • It’s hard to write tests that don’t all have to be written when requirements change.
  • It’s a gradual process to change (show how it helps you, others may or may not follow)   * persistance is needed, may have to throw away old tests
  • you need to have buy in from the team and coverage to make it worthwhile, however, if it helps you get the job done, do it
  • UI Tests are helpful for confidence, but brittle and take maintaining
  • It’s about more than tests, it brings in ideas of architecture, documentation, Single responsibility, dependency injection, composition over inheritance

A few whys

  • Validate business goals
  • Quick feedback => confidence => less FDD
  • Less UI clicking
  • Faster releases
  • Think it through
  • Am I really done?
  • Your team, or yourself next time
  • Avoid regression issues
  • Architecture nudges
  • Get back in the flow

What do I test?

  • The Policies (aka Business Rules from Clean Architecture)​
  • Logic (ifs, formatting, switches)
  • Order by, filtering
  • Method was called with correct values
  • Handles Error states
  • “if the cyclomatic complexity is 1, then don’t test it” Steve Smith”

Where to start

Play find a seam

Http calls

using(var httpClient = new HttpClient()) {}

File System

using (FileStream fs = File.OpenRead(path))

Getting Started - a walk through

  1. What are the business goals and requirements?
    1. Given a new ride is submitted When missing a value Then it should return a 503
  2. Name it - Class_Method_Condition_Assert 1. (BikeController_SaveRide_NoDate_Returns503)
    1. Start with method arguments
  3. // Arrange // Act // Assert
  4. Assert.IsFalse(true)
  5. Factory to create
  6. Remove is false, Assert.AreEqual(503, x), Assert.AreEqual(“Date is required”, y)
  7. Return 503 and hard coded message (passes!)
  8. What is the request object? - change to take in the parameter, change to actually assert the date is there)
  9. Note (in TODO doc) What about invalid dates? Are there business rules?
  10. Refactor to move validation code out of method?
  11. On to the next tests

API Test - Using MOQ Demo

Steps to a test

I created some tests in preparation of doing a live coding session in my presentation. Here is my “script” that I’ll be following. I’ll start in the presentationStart branch. I’ve added snippets of code that I removed for reference as I’m coding.

I’m following a TDD approach with showing the Red > Green > refactoring loop. See Kent Beck’s Test Driven Development for a more in-depth example.

Microsoft docs on Testing MVC Controllers

Update in August, 2019, I’ve since given a few workshops since my first presentation. Instead of live coding, I’ll be using the workshop branch and doing more un-commenting of code. This is more straightforward and less stressful. I’m leaving the steps below, so you can see my thought process as I’d approach this in a TDD manner.

First test - WeatherController_GetCurrentTemp_NoZipCode_Returns400

Given an API call
When asking for current temp and no zip code is given
Then returns a 400
  1. Create WeatherController.cs
  2. Create WeatherControllerTests.cs
  3. Create TestCategories with “Weather API”
  4. Create the basic Factory method
  5. Turn on Live Unit Testing
  6. Create WeatherControllerTests.WeatherController_GetCurrentTemp_NoZipCode_Returns400
    1. reference web project.
    2. Nuget > Microsoft.AspNetCore.App
    3. aaa VS snippet is helpful
    4.  // Arrange
       // Act
       // Assert
      
    5. Assert.Inconclusive();
    6. var result = await controller.CurrentTemp(0);
    7. Need to create the method in WeatherController (ctrl + .)
    8. how to handle async?
      1. change CurrentTemp to return Task.FromResult(52.5); to get it to build
      2. async Task the methods and test call
    9. Assert.AreEqual(52.5, result);
    10. run => passed
    11. But we want to return a bad request for zip code = 0!
      1. replace assert with Assert.AreEqual(400, (result.Result as BadRequestObjectResult).StatusCode);
  7. Write the code to pass the test (if 0 then return BadRequest)
    1. change to public async Task<ActionResult<double>> CurrentTemp(int zipCode)
    2. if zipCode = 0 check
    3. return Ok(10);
  8. Add TODO - hard coded return value?
  9. Add TODO - verify other zip codes that are invalid, but not 0
  10. test passes!
  11. review what we did

Next Test - WeatherController_GetCurrentTemp_ZipCode_CallsWithZipCode

    Given an API call
    When asking for current temp
    Then it calls the weather Api with the correct zip code
  1. Add test to WeatherControllerTests
  2. var zipCode = 57105;
  3. var (weatherController, getWeatherHttpClient) = Factory();
  4. update Factory
  5. update WeatherController ctor
  6. Assert.IsTrue(false); => fail
  7. Pause, what is IGetWeatherHttpClient and HttpClientFactory?
  8. HttpClientFactory in .Net Core
    1. Http Request docs 1. HttpClientFactory article
  9. Pause: using MOQ (introduction, example) to create a fake response, why?
    1. Create a test seam
    2. fake the dependency
    3. verify it was called correctly
    4. avoid writing custom code for the test seam
  10. Interface for typed HttpClient in ApixuClient
  11. // TODO add services.AddHttpClient<IGetWeatherHttpClient, ApixuClient>(); to Startup
    1. no ApixuClient implementation needed yet, just the interface
    2. We are testing the WeatherController, not the client yet
    3. I’ve already injected it, but the Startup.cs needs to use the DI system to inject it 1.Fake the response in the test
    4. var fakeTemp = 72.6; getWeatherHttpClient.Setup(wp => wp.GetCurrentTempAsync(zipCode)).ReturnsAsync(fakeTemp);
  12. Now we can call a method, wait we don’t have one yet
  13. // Act, call the WeatherController Method
    1. var response = await weatherController.CurrentTemp(zipCode);
  14. Change Assert to verify with MOQ
    1. getWeatherHttpClient.Verify(w => w.GetCurrentTempAsync(zipCode), Times.Once);
    2. fails
  15. add call to the client
    1. add TODO : We can’t stay with hard coded values so we need something to call a real service
    2. var result = await weatherHttpClient.GetCurrentTempAsync(zipCode); return Ok(result);
  16. pass!!

Note about some tests may be throwaway-able test. My tweet

My colleague says to ask “When will this break?” If it’s not really going to save you from anything you didn’t intentionally do, then don’t do it. (I think it still may be useful for TDD, but maybe you delete it later?)

Bring in a real http call

Now that we have tested that the weather controller is calling the http client code, we need an actual implementation. I’m using Apixu for a free weather api. It was one of the first to jump up in my Bing search.

  1. Create a ApixuClientTests.cs file
  2. Add ApixuClientTests_GetTemp_GivenAZipCode_ReturnsTemp
  3. Assert.IsTrue(false) => fail/Red
  4. ApixuClient class is already there in this branch, let’s test it
  5. use the factory and setup a fake response
    1. using the Nuget package MockHttp after reading through Github issues for a couple hours of being stuck and trying things. You should expect to be stuck for a few hours every once in awhile as you are creating new testing seams.
    2. I called the api through their website, then used quicktype to create the C# classes from the JSON result.
  6. // Act, call the method, but it doesn’t exist
    1. var result = await apiuxClient.GetCurrentTempAsync(zipCode);
  7. Create the GetCurrentTempAsync method
    1. return Task.FromResult(65.4);
  8. still failing, make it pass
  9. Assert.AreEqual(fakeTemp, result)
  10. implement that call
    var response = await httpClient.GetStringAsync($"current.json?key={apiKey}&q={zipCode}");
    var weather = ApiuxWeatherCurrentResponse.FromJson(response);
    return weather.Current.TempF;
  1. add TODO: verify the client was called with the correct key and zipCode
  2. add TODO: do I need to add a zip code validation here or is having it in the controller enough
    1. if so, refactor zip code validation to a small reusable class/method.
    2. change name to WeatherController_GetPastTemp_NoDateTimeOrInvalid_Returns400
    3. Note: DataRow is really helpful for this
  3. Test passes!!

It’s time to stop, but we are having so much fun!!

Unit testing WebAPi

Check out the MS Docs on unit testing Controllers in .Net Core .

You need to cast the actionResult to an OkObjectResult or NotFoundObjectResult or BadRequestObjectResult. var result = actionResult as OkObjectResult; then Assert.AreEqual(1, result.Value);

This would probably be a better video, so I’d recommend the MVA course and/or following along with Kent Beck’s TDD book.

API Test With EF Core InMemoryDatabase DEMO

I didn’t get to this, we’ll see what the future holds.

Testing with InMemory in EF Core avoids the need to Fake out EF parts and can help make sure all the LINQ statements are correct. However, it is a bit slower then faking and you have to do a manually add before a .Include usage, so the choice is yours.

Here are a few hints:

Factory

public class SettingsControllerTests
{
    private static (SettingsController settingsController, SettingsDbContext settingsDbContext) Factory(string testDbName)
    {
        var dbContext = SetupDatabase(testDbName);
        var loggerFake = new Mock<ILogger<SettingsController>>();
        return (new SettingsController(dbContext, loggerFake.Object), dbContext);
    }
....

Add a SetupDatabase method.

private static SettingsDbContext SetupDatabase(string testDbName)
{
    var options = new DbContextOptionsBuilder<SettingsDbContext>()
        .UseInMemoryDatabase(databaseName: testDbName)
        .Options;

    return new SettingsDbContext(options);
}

Use in your test

[TestMethod]
public async Task SettingsController_Get_ReturnsValuesAsync()
{
    // Arrange
    var (controller, settingsDbContext) = Factory(System.Reflection.MethodBase.GetCurrentMethod().Name);
    var setting = new Setting
    {
        Id = 1,
        Name = "test1",
        Value = "valueTest1",
        Description = "descriptionTest"
    };
    settingsDbContext.Settings.Add(setting);
    settingsDbContext.SaveChanges();

    // Act
    var actionResult = await controller.Get(CancellationToken.None);
    var result = actionResult as OkObjectResult;
    var value = result.Value as IList<Setting>;

    // Assert
    Assert.AreEqual(1, value.Count);
}

UI (JavaScript) Jest test DEMO

I didn’t get to this, we’ll see what the future holds.

I’d like to build a few demo tests for VueJs, using Jest and running them in KarmaJs on multiple browsers.

Resources

In an effort to reduce duplication see my original post on Unit Testing .

Ready to Go

  • Go out there and try
  • Learn more with your teams
  • Maybe you’ll need to be the “agent of change”
  • Create better feedback and safety nets for your team

Presentation Timeline for 50 minutes with questions

00:00 Introduction and sponsor thank you 01:00 Terms 02:00 Three Main Camps 04:00 A Journey, My Experiences 08:00 A few whys 10:00 What do I test? 12:00 Where to start? 15:00 Create a Test Seam - Http Call 20:00 Getting Started - a walkthrough 25:00 API Test - Using MOQ DEMO 46:00 Resources 48:00 Ready to Go? 50:00 Questions & Thank you 55:00 done!

More things I’d like to do with this project

(aka if only there was more time :-))

  • Azure DevOps build and pipeline
  • Build out the UI with VueJs or Blazor
  • Make it a PWA
  • add in UI tests
  • use Puppeteer or Cypress for end-to-end tests in the UI

I recorded the October 27th, 2018 talk on my Pixel phone. It turned out pretty good. Here is the raw audio .

Me before my NE Code 2018 presentation



Watch the Story for Good News
I gladly accept BTC Lightning Network tips at [email protected]

Please consider using Brave and adding me to your BAT payment ledger. Then you won't have to see ads! (when I get to $100 in Google Ads for a payout, I pledge to turn off ads)

Use Brave

Also check out my Resources Page for referrals that would help me.


Swan logo
Use Swan Bitcoin to onramp with low fees and automatic daily cost averaging and get $10 in BTC when you sign up.