Let Tests Tell a Story With utPLSQL Nested Contexts

There’s a great talk by Kevlin Henney about Structure and Interpretation of Test Cases that inspired me to rethink my approach of writing tests.

With the features added in the latest version 3.1.9 of utPLSQL, this approach is even easier to pull off as I want to showcase:

We start with the friend or foe detection system of the deathstar, which decides based upon the weapon and clothing of indiviudals whether they are friend or foe to the empire (you know: red lightsabers mean sith, which are friends, blue or green lightsabers mean jedi, which are foes etc.)

The full example is available on my github repo as always, I’ll skip some parts here.

create or replace package deathstar_security as

  /* Decides whether a person is friend or foe,
     based on their appearance
   */
  function friend_or_foe( i_person_data t_person_appearance )
    return varchar2;
end;
/

Let’s have a basic set of unit-tests now to cover the most important paths of the functionality
(I’ll skip to share the body here because it’s really just a straight forward
ut.expect(deathstar_security.friend_or_foe(parameter))
.to_equal('FRIEND')
comparison)

create or replace package ut_deathstar_friend_or_foe as
  -- %suite(Friend or Foe detection)
  -- %suitepath(ut_deathstar.defense)

  -- %test(Red lightsaber means friend)
  procedure lightsaber_red_means_friend;

  -- %test(Blue lightsaber means foe)
  procedure lightsaber_blue_means_foe;

  -- %test(Black robe means friend)
  procedure robe_black_means_friend;

  -- %test(Brown robe means foe)
  procedure robe_brown_means_foe;

  -- %test(Red robe means unknown)
  procedure robe_red_means_unknown;

  -- %test(White armor means friend)
  procedure armor_white_means_friend;

  -- %test(Orange armor means foe)
  procedure armor_orange_means_foe;

  -- %test(Blue/Black armor means foe)
  procedure armor_blue_black_means_foe;

  -- %test(Green armor means unknown)
  procedure armor_green_means_unknown;
end;
/

/* Insert Body here - see github repo */

select * from table(ut.run('ut_deathstar_friend_or_foe'));

This will give the following output:

ut_deathstar
  defense
    Friend or Foe detection
      Red lightsaber means friend [,029 sec]
      Blue lightsaber means foe [,004 sec]
      Black robe means friend [,004 sec]
      Brown robe means foe [,003 sec]
      Red robe means unknown [,003 sec]
      White armor means friend [,004 sec]
      Orange armor means foe [,003 sec]
      Blue/Black armor means foe [,004 sec]
      Green armor means unknown [,004 sec]
 
Finished in ,066423 seconds
9 tests, 0 failed, 0 errored, 0 disabled, 0 warning(s)

A pretty usual test result. We have a test suite that does several checks on our expected friend/foe detection functionality.

But what do the tests tell the reader? Do we get an idea about what the functionality is actually meant to do?

Consider the following output in comparison:

ut_deathstar
  defense
    Friend or Foe detection
      When a person has a lightsaber
        that is red: FRIEND [,026 sec]
        that is blue: FOE [,004 sec]
      When a person has no lightsaber
        but is wearing a robe
          that is black: FRIEND [,005 sec]
          that is brown: FOE [,004 sec]
          that is red: UNKNOWN [,004 sec]
        but is wearing armor
          that is white: FRIEND [,004 sec]
          that is orange: FOE [,005 sec]
          that is blue/black: FOE [,005 sec]
          that is green: UNKNOWN [,004 sec]
 
Finished in ,077354 seconds
9 tests, 0 failed, 0 errored, 0 disabled, 0 warning(s)

Do you notice a difference?
I’m pretty sure that you got a far better understanding what the friend/foe detection is supposed to do by reading the second example.

This is because the tests are now structured in a more logical way, and hold additional context about the relations between different parameters.
For example, we immediately see now that if someone wields a lightsaber, this is the only criteria that will be considered.

We changed absolutely nothing in the tests themselves, they are still exactly the same, but we used the utPLSQL context annotation and slightly different test descriptions to actually tell a story about our functionality.

create or replace package ut_deathstar_friend_or_foe as
  -- %suite(Friend or Foe detection)
  -- %suitepath(ut_deathstar.defense)

  -- %context(When a person has a lightsaber)
    -- %name(2_lightsaber)

    -- %test(that is red: FRIEND)
    procedure lightsaber_red_means_friend;
    -- %test(that is blue: FOE)
    procedure lightsaber_blue_means_foe;
  -- %endcontext

  -- %context(When a person has no lightsaber)
    -- %name(1_no_lightsaber)

    -- %context(but is wearing a robe)
      -- %name(robe)

      -- %test(that is black: FRIEND)
      procedure robe_black_means_friend;
      -- %test(that is brown: FOE)
      procedure robe_brown_means_foe;
      -- %test(that is red: UNKNOWN)
      procedure robe_red_means_unknown;
    -- %endcontext

    -- %context(but is wearing armor)
      -- %name(armor)

      -- %test(that is white: FRIEND)
      procedure armor_white_means_friend;
      -- %test(that is orange: FOE)
      procedure armor_orange_means_foe;
      -- %test(that is blue/black: FOE)
      procedure armor_blue_black_means_foe;
      -- %test(that is green: UNKNOWN)
      procedure armor_green_means_unknown;
    -- %endcontext;

  -- %endcontext
end;
/

It’s not that difficult, is it?
Well, not syntax-wise at least.

I experienced it to be more difficult to write tests in that way, but the benefits for a reader, especially when they don’t not know the codebase (maybe your future self?), are immense!

There are two things to mention about this code-snippet:

  • The empty line before the first test or context annotation is necessary to tell utPLSQL the scope of any following annotations (some annotations can be used in different scopes, e.g. displayname)
  • We start the name annotation with a digit and give the context we want to appear first the highest number. This is due to a minor bug in the current utPLSQL release. Normally, the contexts should be done according to their appearance in the code, at the moment they are done by their name in descending order.

You can find the whole example on my github repository.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s