Testing Elixir - Pragmatic Bookshelfmedia.pragprog.com/titles/lmelixir/isolate.pdf · allowing any...

12
Extracted from: Testing Elixir Effective and Robust Testing for Elixir and its Ecosystem This PDF file contains pages extracted from Testing Elixir, published by the Prag- matic Bookshelf. For more information or to purchase a paperback or PDF copy, please visit http://www.pragprog.com. Note: This extract contains some colored text (particularly in code listing). This is available only in online versions of the books. The printed versions are black and white. Pagination might vary between the online and printed versions; the content is otherwise identical. Copyright © 2020 The Pragmatic Programmers, LLC. All rights reserved. No part of this publication may be reproduced, stored in a retrieval system, or transmitted, in any form, or by any means, electronic, mechanical, photocopying, recording, or otherwise, without the prior consent of the publisher. The Pragmatic Bookshelf Raleigh, North Carolina

Transcript of Testing Elixir - Pragmatic Bookshelfmedia.pragprog.com/titles/lmelixir/isolate.pdf · allowing any...

  • Extracted from:

    Testing ElixirEffective and Robust Testing for Elixir and its Ecosystem

    This PDF file contains pages extracted from Testing Elixir, published by the Prag-matic Bookshelf. For more information or to purchase a paperback or PDF copy,

    please visit http://www.pragprog.com.

    Note: This extract contains some colored text (particularly in code listing). Thisis available only in online versions of the books. The printed versions are blackand white. Pagination might vary between the online and printed versions; the

    content is otherwise identical.

    Copyright © 2020 The Pragmatic Programmers, LLC.

    All rights reserved.

    No part of this publication may be reproduced, stored in a retrieval system, or transmitted,in any form, or by any means, electronic, mechanical, photocopying, recording, or otherwise,

    without the prior consent of the publisher.

    The Pragmatic BookshelfRaleigh, North Carolina

    http://www.pragprog.com

  • Testing ElixirEffective and Robust Testing for Elixir and its Ecosystem

    Andrea LeopardiJeffrey Matthias

    The Pragmatic BookshelfRaleigh, North Carolina

  • Many of the designations used by manufacturers and sellers to distinguish their productsare claimed as trademarks. Where those designations appear in this book, and The PragmaticProgrammers, LLC was aware of a trademark claim, the designations have been printed ininitial capital letters or in all capitals. The Pragmatic Starter Kit, The Pragmatic Programmer,Pragmatic Programming, Pragmatic Bookshelf, PragProg and the linking g device are trade-marks of The Pragmatic Programmers, LLC.

    Every precaution was taken in the preparation of this book. However, the publisher assumesno responsibility for errors or omissions, or for damages that may result from the use ofinformation (including program listings) contained herein.

    Our Pragmatic books, screencasts, and audio books can help you and your team createbetter software and have more fun. Visit us at https://pragprog.com.

    For sales, volume licensing, and support, please contact [email protected].

    For international rights, please contact [email protected].

    Copyright © 2020 The Pragmatic Programmers, LLC.

    All rights reserved. No part of this publication may be reproduced, stored in a retrieval system,or transmitted, in any form, or by any means, electronic, mechanical, photocopying, recording,or otherwise, without the prior consent of the publisher.

    ISBN-13: 978-1-68050-782-9Encoded using the finest acid-free high-entropy binary digits.Book version: B1.0—June 3, 2020

    https://[email protected]@pragprog.com

  • Isolating CodeThere are multiple strategies we can employ to test our code in a way that wecan remove outside variables, controlling the situation in which our codeunder test must perform and allowing us to expect a specific outcome. Wewill cover the easiest method—injecting dependencies and creating basicsubstitutes (test doubles)—in order to add another option to your testing tool-belt.

    Dependency InjectionBefore we can leverage dependency injection to isolate the behavior of ourcode under test, let’s take a moment to define dependency injection. Adependency is any code that your code relies on. Dependency Injection (oftenabbreviated as DI) is a fancy name for any system that allows your code toutilize a dependency without hard-coding the name of the dependency,allowing any unit of code that meets a contract to be used. In Elixir, we havetwo common ways to inject a dependency: as a parameter to a function andthrough the application environment. Utilizing DI in our tests allows us tocreate replacement dependencies that behave in predictable ways, allowingthe tests to focus on the logic inside the code under test.

    Passing a dependency as a parameter is the more common way to injectdependencies in unit testing, and that is the style that we will focus on first.In later chapters, we will cover dependency injection via the applicationenvironment, as well as Mox, a tool that aids in creating test doubles.

    Test double terminology

    In this chapter, we will be referring to stand-ins for productioncode for purposes of testing as "test doubles." This covers codethat can either simply return a prescribed result, or additionallyassert that it was called, with or without specific parameters. Inthe next chapter, we will dive deeper into different kinds of doubles.

    Passing a Dependency as a ParameterPassing a dependency as a parameter is as straightforward as it sounds andis often the simplest solution. We can choose to either pass a function or amodule as a parameter. When you inject a module as shown in the followingcode, it must meet an implicit contract or calling the code will raise anexception. The SoggyWaffle module has been written to allow a weather forecastfunction to be passed in.

    • Click HERE to purchase this book now. discuss

    http://pragprog.com/titles/lmelixirhttp://forums.pragprog.com/forums/lmelixir

  • unit_tests/soggy_waffle/lib/soggy_waffle.exdefmodule SoggyWaffle doLine 1

    alias SoggyWaffle.WeatherAPI-def rain?(city, datetime, weather_fn \\ &WeatherAPI.get_forecast/1) do-

    with {:ok, response}

  • weather_fn_double = fn city ->10send(self(), {:get_forecast_called, city})-

    -

    data = [-%{-

    "dt" => future_unix,15"weather" => [%{"id" => _drizzle_id = 300}]-

    }-]-

    -

    {:ok, %{"list" => data}}20end-

    -

    assert SoggyWaffle.rain?(expected_city, now, weather_fn_double)--

    assert_received {:get_forecast_called, ^expected_city},25"get_forecast/1 was never called"-

    end-end-

    end-

    The major feature of this test is the definition and use of a function-style testdouble, weather_fn_double, at line 10. It matches the contract of SoggyWaffle.Weather-API.get_forecast/1 but, unlike the real function, it will always return the exactsame response. Now our test can assert on specific values, helping us to knowthat the module under test, SoggyWaffle, is behaving correctly.

    Notice, though, that the test double doesn’t just meet the contract, it alsohas a mechanism to report that the function was called and to send theparameter passed to it back to the test, seen at line 11. Because the doubleis defined inside of the test code, not inside the anonymous function, self()will be the pid for the test process. When the function is called, the modulewill send the test process the {:get_forecast_called, city} message.

    The test code at line 25 makes sure that the function was called, but alsothat the correct value was passed to it. We call this style of testing an expecta-tion. This way, if that dependency is never called, the test will fail. This kindof expectation is mostly useful when there is an expected side effect of yourcode and your tests are concerned with making sure it happens. Admittedlyour test is just making a query via the function call, so the expectation is nottotally necessary in this case, but it still illustrates well how to add anexpectation to a test double.

    There are some other features of this test that are worth noting before wemove on. The code under test uses time comparisons in its business logic,actually found in a functional dependency, SoggyWaffle.Weather. Because wearen’t isolating SoggyWaffle from that dependency, knowledge of the time com-

    • Click HERE to purchase this book now. discuss

    Isolating Code • 3

    http://pragprog.com/titles/lmelixirhttp://forums.pragprog.com/forums/lmelixir

  • parison has now bled up into the the knowledge needed to test SoggyWaffle.This ties back to our earlier discussion of defining the unit, or black box, forour test. Because SoggyWaffle.Weather is well tested, there is little downside totaking this approach as it will prove to be easier to understand and maintainin the long run, a very important priority when we design our tests.

    The anonymous function is defined inside of the test at line 10, as opposedto a named function in the same file, because it needs to have access to thevalue bound to future_unix. Additionally, we have to define the test’s pid outsideof the function because self() wouldn’t be evaluated until the function is exe-cuted, inside the process of the code under test, which could have a differentpid than our test. Because anonymous functions are closures in Elixir, thevalue of future_unix is part of the function when it gets executed. The call toself() is evaluated at the definition of the anonymous function, with the valuebeing the pid of that test process. Leveraging closures is one of the advantagesof function-style dependency injection. There are other DI tools we willexamine later that give us similar power.

    One last, notable feature of this test that isn’t related to dependency injectionis the use of randomized data at line 8. When test data is incidental, meaningit shouldn’t change the behavior of your code, try to avoid hard-coded values.Don’t worry about this when you are making your first pass on a test, butbefore you consider that test complete, look for places where you can switchfrom hard-coded values to randomized data.

    While you aren’t looking to try to test your code with every possible value thatcould be passed to it (that’s more inline with property-based testing, coveredin Chapter 7, Property-based Testing, on page ?), it is possible to accidentallywrite code that only passes with a single specific input. That’s obviously notideal, so it’s nice to reach for the low-hanging fruit and add a little variationto what is passed in. There is no benefit to testing all of the city options inthis case because the value of the string itself won’t change anything. It isjust being passed to the weather function. Our test’s concern is that the correctvalue is passed to double that we injected. While we have a hard-coded listof possible cities, there are libraries that can help generate random data, likeFaker,3 but even handling it locally like we did here will net you the benefitswithout having to pull in a new dependency.

    Be careful, though. The pursuit of dynamic tests like this can go too far,leaving your test hard to understand and hard to maintain. Additionally, the

    3. https://hex.pm/packages/faker

    • 4

    • Click HERE to purchase this book now. discuss

    https://hex.pm/packages/fakerhttp://pragprog.com/titles/lmelixirhttp://forums.pragprog.com/forums/lmelixir

  • test must have a way to know what data it is using and the assertions shouldbe able to take that into account.

    Finer Control Over Dependency InjectionAn alternative to injecting a whole function is to pass in a single value. Anexample that you might see in code is when the outside dependency is thesystem time. If your code under test needs to do something with system time,it makes it very difficult for your tests to assert a known response or valueunless you can control system time. While controlling your code’s concept ofsystem time is easy in some languages, it is not in Elixir. That makes a sce-nario like this a perfect candidate for injecting a single value. The followingcode allows for an injected value, but defaults to the result of a function callif no parameter is passed.

    unit_tests/soggy_waffle/lib/soggy_waffle/weather.exdefmodule SoggyWaffle.Weather doLine 1

    @type t :: %__MODULE__{}--

    defstruct [:datetime, :rain?]-5

    @spec imminent_rain?([t()], DateTime.t()) :: boolean()-def imminent_rain?(weather_data, now \\ DateTime.utc_now()) do-

    Enum.any?(weather_data, fn-%__MODULE__{rain?: true} = weather ->-

    in_next_4_hours?(now, weather.datetime)10-

    _ ->-false-

    end)-end15

    -

    defp in_next_4_hours?(now, weather_datetime) do-four_hours_from_now =-

    DateTime.add(now, _4_hours_in_seconds = 4 * 60 * 60)-20

    DateTime.compare(weather_datetime, now) in [:gt, :eq] and-DateTime.compare(weather_datetime, four_hours_from_now) in [:lt, :eq]-

    end-end-

    Looking at our function signature at line 7, we can see that without a valuepassed in, the result of DateTime.utc_now/0 will be bound to the variable datetime.Our tests will pass a value in, overriding the default, but when running inproduction, the code will use the current time. By allowing our function totake a known time, we can remove the unknown. When running in production,your code will never be passed another value, but testing it just got a lot

    • Click HERE to purchase this book now. discuss

    Isolating Code • 5

    http://media.pragprog.com/titles/lmelixir/code/unit_tests/soggy_waffle/lib/soggy_waffle/weather.exhttp://pragprog.com/titles/lmelixirhttp://forums.pragprog.com/forums/lmelixir

  • easier. Our tests can now create a controlled environment in which to testour code.

    unit_tests/soggy_waffle_examples/test/soggy_waffle/weather_test.exsdefmodule SoggyWaffle.WeatherTest doLine 1

    use ExUnit.Case-alias SoggyWaffle.Weather-

    -

    describe "imminent_rain?/2" do5test "returns true when it will rain in the future" do-

    now = datetime(hour: 0, minute: 0, second: 0)-one_second_from_now = datetime(hour: 0, minute: 0, second: 1)-

    -

    weather_data = [weather_struct(one_second_from_now, :rain)]10-

    assert Weather.imminent_rain?(weather_data, now) == true-end-

    Our code under test is all time-based, with “the future” being very important.By passing in a value, we are able to strictly control the conditions in whichour code is executed, isolating our code under test from its dependency,system time (via DateTime.utc_now/0). This is especially important because weneed alignment between “now”, when the code is executing, and the time inthe weather data passed into the function.

    Notice that there is a one second difference between “now” and the weatherdata. This is a practice called boundary testing. Our code is looking for valuesin the future and we have made the future as close to the boundary as wecan so that we can be certain that any time more recent than that will alsopass the test. While technically, our data could have been 1 microsecond inthe future, the data our application will get from the weather API is granularonly down to the second, so we will stick with a unit of time that most peopleare more familiar with, the second. Whenever you are writing tests for codethat does comparisons, you should strive to test right at the boundaries. Thisis even more important if you are dealing with any timezone based logic astesting too far from a boundary can hide bad timezone logic.

    In our test, there are calls to two helper functions, datetime/1 and weather_struct/2.They can be explained fairly easily: datetime/1 returns a %DateTime{} struct whereall the values are the same each time except the overrides for hour, minute,and second, while weather_struct/2 returns SoggyWaffle.Weather structs as definedin the module of the same name in our application. These allow us to easilyconstruct test data in a way that improves the readability of the test. Let’ssee the definitions for these helper functions.

    unit_tests/soggy_waffle_examples/test/soggy_waffle/weather_test.exsdefp weather_struct(datetime, condition) do

    • 6

    • Click HERE to purchase this book now. discuss

    http://media.pragprog.com/titles/lmelixir/code/unit_tests/soggy_waffle_examples/test/soggy_waffle/weather_test.exshttp://media.pragprog.com/titles/lmelixir/code/unit_tests/soggy_waffle_examples/test/soggy_waffle/weather_test.exshttp://pragprog.com/titles/lmelixirhttp://forums.pragprog.com/forums/lmelixir

  • %Weather{datetime: datetime,rain?: condition == :rain

    }end

    defp datetime(options) do%DateTime{

    calendar: Calendar.ISO,day: 1,hour: Keyword.fetch!(options, :hour),microsecond: {0, 0},minute: Keyword.fetch!(options, :minute),month: 1,second: Keyword.fetch!(options, :second),std_offset: 0,time_zone: "Etc/UTC",utc_offset: 0,year: 2020,zone_abbr: "UTC"

    }end

    Be careful, though, as helper functions like this can become a maintenancenightmare. When writing helper functions for your tests, try to keep themdefined in the same file as the tests using them, and try to keep them simplein functionality, with clear, explanatory names. If you design the helperfunction’s signature to enhance the readability of your tests, all the better.In the case of our datetime/1, the signature takes a keyword list, letting the callitself highlight what is important about the return value.

    The keys aren’t necessary since plain values would have sufficed, but theymake it fairly easy to understand the difference in the return values at lines7 and 8 of the test body from the SoggyWaffle.WeatherTest code sample on page6. All the other values will be the same. By contrast, weather_struct/2 only takesplain values, but intentional parameter naming, both of the variable nameand that atom, still keep the call easy to understand.

    For unit testing, injecting dependencies through the API will help keep yourcode clean and your tests easy and controlled. As you move on to dealingwith integration testing, you will need to explore other methods of dependencyinjection since you won’t necessarily have the ability to pass a dependencyin from your tests like you would with a unit test. Both of these ways of doingit are simple and easy to understand. Ultimately, that is the best argumentfor using them.

    • Click HERE to purchase this book now. discuss

    Isolating Code • 7

    http://pragprog.com/titles/lmelixirhttp://forums.pragprog.com/forums/lmelixir