ColdFrame: Testing Classes



AUnit

The discussion here is based on the use of AUnit.

AUnit has the concept test case; for each test case there is a fixture, which corresponds to the standard setup that each test procedure in the test case expects.

The fixture is implemented using the test case operations Set_Up and Tear_Down.

ColdFrame supports Set_Up using its standard Domain.Initialize (and Cascade_Initialize) procedures.

ColdFrame supports Tear_Down by providing Domain.Tear_Down (and Cascade_Tear_Down) procedures.

The examples/ and test/ subdirectories contain AUnit tests of aspects of ColdFrame's functionality. Note that these tests are written using version 3 of AUnit, as supplied with GNAT GPL 2011, and will not work with the Debian-supplied software (up to at least wheezy) because it provides only AUnit 1.03.

Access to private information

ColdFrame takes a fairly restrictive attitude to the visibility of constructs in the generated code. For example, only «public» classes are publicly visible outside the domain; all other classes are implemented as private children of the domain package, and the full declaration of a class's instance record is in the private part of the class's package specification.

If you are doing domain-level "unit" tests (that is, tests that involve stubbed functionality from other domains), implement the test suite in a child unit of the domain (perhaps Domain.Unit_Test).

There are two approaches to doing class-level unit tests.

The first is similar to that for domain-level tests: implement the test suite in a child unit of the class (perhaps Domain.Class.Tests).

You may feel that this leads to too many deeply-nested packages. As an alternative, generate the code with the UNIT_TEST_SUPPORT variable set to yes:

$ make Domain.gen UNIT_TEST_SUPPORT=yes

This generates a child package for each class, Domain.Class.Unit_Test, which contains Get_ and Set_ subprograms for each mutable attribute excluding Timers and, for a class with a state machine, Get_State_Machine_State and Set_State_Machine_State subprograms.

Identifying attributes are immutable, and only have a Get_ operation.

Timers are supported by an Access_ function returning an access-to-Timer; you can use the event queue inspection facilities to find out more.

What it does not do is allow you to access private subprograms of the class. A common reason for having private operations is to implement state machines; in this case, you could consider calling the event handler synchronously (just remember to set the state first). It's arguable that by doing this you have a more representative execution path, anyway.

Example

There's a note on generating stubs as used in the examples/House_Management.test/ subdirectory of the source distribution, with additional information on using them. This example uses this feature.

Note that the test suite is a child of the House_Management.Lamp package. This is so that the test code has access to private members of the domain (non-«public» classes and associations, for a start) and to private subprograms of the class.


The main program:

with AUnit.Reporter.Text;
with AUnit.Run;
with House_Management.Lamp.Test_Suite;
with GNAT.Exception_Traces;

procedure House_Management.Harness is

   procedure Run is new AUnit.Run.Test_Runner (Lamp.Test_Suite.Suite);

   Reporter : AUnit.Reporter.Text.Text_Reporter;

begin

   GNAT.Exception_Traces.Trace_On
     (Kind => GNAT.Exception_Traces.Unhandled_Raise);

   Run (Reporter);

end House_Management.Harness;

The test suite spec:

with AUnit.Test_Suites;

package House_Management.Lamp.Test_Suite is

   function Suite return AUnit.Test_Suites.Access_Test_Suite;

end House_Management.Lamp.Test_Suite;

The test suite body:

Standard AUnit withs:

with AUnit.Assertions; use AUnit.Assertions;
with AUnit.Test_Cases; use AUnit.Test_Cases;

Test-specific withs:

with ColdFrame.Project.Events.Standard.Test;
with ColdFrame.Stubs;
with Digital_IO.Initialize;
with Digital_IO.Tear_Down;
with House_Management.Initialize;
with House_Management.Tear_Down;

The actual body:

package body House_Management.Lamp.Test_Suite is

Instantiations of ColdFrame.Stubs generics:

   function Get_Boolean
   is new ColdFrame.Stubs.Get_Input_Value (Boolean);
   function Get_Signal_Name
   is new ColdFrame.Stubs.Get_Input_Value (Digital_IO.Output_Signal);

A test case, for testing Lamp code:

   package Lamps is
      type Case_1 is new Test_Case with private;
   private
      type Case_1 is new Test_Case with null record;
      function Name (C : Case_1) return AUnit.Message_String;
      procedure Register_Tests (C : in out Case_1);
      procedure Set_Up (C : in out Case_1);
      procedure Tear_Down (C : in out Case_1);
   end Lamps;

The test case body:

   package body Lamps is

A test procedure spec (GNAT with style checks enabled, -gnaty, complains if a subprogram has no spec):

      --  Check that each Lamp is connected to the correct Signal in
      --  the Digital_IO domain, in the correct sense (ie, turning the
      --  Lamp on sets the Signal to True).
      procedure Turn_On (R : in out AUnit.Test_Cases.Test_Case'Class);

The test procedure body:

      procedure Turn_On (R : in out AUnit.Test_Cases.Test_Case'Class) is
         pragma Unreferenced (R);
         use type Digital_IO.Output_Signal;
      begin

         --  Initialization creates a number of lamps; each one turns
         --  itself off on creation (to be tested elsewhere). However,
         --  we have to account for them; we can't just not initialize
         --  the domain, because initialization creates all the
         --  domain's singletons, initialises «class» attributes,
         --  and calls user {init} operations to, amongst other
         --  things, set up 'specification' instances and
         --  associations.
         Assert (ColdFrame.Stubs.Number_Of_Calls
                   ("Digital_IO.Application.Set_Output") = 4,
                 "wrong number of calls");

         --  Turn on the Basement lamp.
         Lamp.Turn_On (Lamp.Find ((Name => Basement)));
         --  There should have been 5 calls now.
         Assert (ColdFrame.Stubs.Number_Of_Calls
                   ("Digital_IO.Application.Set_Output") = 5,
                 "wrong number of calls (a)");
         --  The 5th call should have been for Lamp D ...
         Assert (Get_Output_Signal ("Digital_IO.Application.Set",
                                    "O",
                                    5) = 3,
                 "wrong signal (a)");
         --  ... and it should have been turned on.
         Assert (Get_Boolean ("Digital_IO.Application.Set",
                              "To_State",
                              5),
                 "should have been turned on (a)");

         --  Repeat for the remaining Lamps.
         Lamp.Turn_On (Lamp.Find ((Name => Ground_Floor)));
         Assert (ColdFrame.Stubs.Number_Of_Calls
                   ("Digital_IO.Application.Set") = 6,
                 "wrong number of calls (b)");
         Assert (Get_Output_Signal ("Digital_IO.Application.Set",
                                  "S",
                                  6) = 2
                 "wrong signal (b)");
         Assert (Get_Boolean ("Digital_IO.Application.Set",
                              "To_State",
                              6),
                 "should have been turned on (b)");

         Lamp.Turn_On (Lamp.Find ((Name => First_Floor)));
         Assert (ColdFrame.Stubs.Number_Of_Calls
                   ("Digital_IO.Application.Set") = 7,
                 "wrong number of calls (c)");
         Assert (Get_Output_Signal ("Digital_IO.Application.Set",
                                  "S",
                                  7) = 1,
                 "wrong signal (c)");
         Assert (Get_Boolean ("Digital_IO.Application.Set",
                              "To_State",
                              7),
                 "should have been turned on (c)");

         Lamp.Turn_On (Lamp.Find ((Name => Second_Floor)));
         Assert (ColdFrame.Stubs.Number_Of_Calls
                   ("Digital_IO.Application.Set") = 8,
                 "wrong number of calls (d)");
         Assert (Get_Output_Signal ("Digital_IO.Application.Set",
                                  "S",
                                  8) = 0,
                 "wrong signal (d)");
         Assert (Get_Boolean ("Digital_IO.Application.Set",
                              "To_State",
                              8),
                 "should have been turned on (d)");

      end Turn_On;

The test case name:

      function Name (C : Case_1) return AUnit.Message_String  is
         pragma Unreferenced (C);
      begin
         return new String'("Lamps.Case_1");
      end Name;

Test procedure registration (only the one test procedure so far!):

      procedure Register_Tests (C : in out Case_1) is
      begin
         Register_Routine
           (C,
            Turn_On'Access,
            "turn on");
      end Register_Tests;

The test case set-up:

      procedure Set_Up (C : in out Case_1) is
         pragma Unreferenced (C);
         Q : constant ColdFrame.Project.Events.Event_Queue_P
           := new ColdFrame.Project.Events.Standard.Test.Event_Queue;
      begin
         ColdFrame.Stubs.Set_Up;
         Digital_IO.Initialize (Q);
         House_Management.Initialize (Q);
      end Set_Up;

The test case tear-down:

      procedure Tear_Down (C : in out Case_1) is
         pragma Unreferenced (C);
      begin
         House_Management.Tear_Down;
         Digital_IO.Tear_Down;
         ColdFrame.Stubs.Tear_Down;
      end Tear_Down;

End of test case:

   end Lamps;

The test suite function:

   function Suite return AUnit.Test_Suites.Access_Test_Suite is
      Result : constant AUnit.Test_Suites.Access_Test_Suite
        := new AUnit.Test_Suites.Test_Suite;
   begin
      AUnit.Test_Suites.Add_Test (Result, new Lamps.Case_1);
      return Result;
   end Suite;

End of test suite:

end House_Management.Lamp.Test_Suite;

Inspecting event queues

If your domain has state machines (and most will), you'll need to cater for them in your testing. But it can be very difficult to manage a unit test when the state machines are all ticking over with timed events every 10 milliseconds!

Rather than posting events to a running event queue and observing the results, the supported way of dealing with this is via event queue inspection; run the code, using an unstarted event queue, and check that the appropriate events have been posted.

An event queue inspector is provided in ColdFrame.Project.Events.Standard.Inspection.

There's no need to use the Test variant, because the queue mustn't be started. To create an unstarted standard queue, say

Q := new ColdFrame.Project.Events.Standard.Event_Queue_Base
  (Start_Started => False,
   Priority => System.Default_Priority,
   Storage_Size => 20_000);

You can inspect the queue's self events using the functions

function Number_Of_Self_Events (On : Event_Queue_P) return Natural;
function Self_Event (On : Event_Queue_P;
                     At_Index : Positive) return Event_P;

You can inspect the queue's standard events (posted to run 'now') using the functions

function Number_Of_Now_Events (On : Event_Queue_P) return Natural;
function Now_Event (On : Event_Queue_P;
                    At_Index : Positive) return Event_P;

You can inspect the queue's events posted to run after a delay using the functions

function Number_Of_After_Events (On : Event_Queue_P) return Natural;
function After_Event (On : Event_Queue_P;
                      At_Index : Positive) return Event_P;
function How_Long_After (On : Event_Queue_P;
                         At_Index : Positive) return Duration;

You can inspect the queue's events posted to run at a later time using the functions

function Number_Of_Later_Events (On : Event_Queue_P) return Natural;
function Later_Event (On : Event_Queue_P;
                      At_Index : Positive) return Event_P;
function When_Later (On : Event_Queue_P;
                     At_Index : Positive) return Time.Time;

(Time is the time support package used in instantiating ColdFrame.Project.Events, usually ColdFrame.Project.Times).

All of the above access events in order of posting, and raise Not_Found if there is no such event.

Additionally, you can access the event held on a Timer:

function Event_Of (The_Timer : Timer) return Event_P;

(returns null if there isn't one).


Simon Wright