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

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 (note: link may go inactive in 2019).

I’ve shared my PowerPoint which will have a lot of the same information.

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

Goal

That you’ll go back to work on Monday and 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 – Art of Unit Testing
  • 2012-2013 – new project, MVC, KnockoutJS, Jasmine testing   - no build system, fell out of use
  • 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

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 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 interface, not internals
  • 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

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. c# // 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!!

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

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
  • Make it a PWA
  • add in UI tests
  • use Puppeteer for end-to-end tests in the UI


comments powered by Disqus

Please consider using Brave and adding me to your payment ledger. Then you won't have to see ads!

Support me and download Brave!

Use Brave