Time for action – testing a GTK+ module

Let's take our old custom_composite Vala project and put a unit test in it.

  1. As usual, let's create the tests directory and include it in configure.ac and Makefile.am in the top directory.
  2. Split the code into two parts. Use this code in custom_window.vala:
    using GLib;
    using Gtk;
    
    public class CustomWindow : Window
    {
      Entry entry;
      Box box;
      public signal void search_updated(string value);
    
      void show_search_box() {
        entry.show();
        entry.has_focus = true;
      }
    
      void hide_search_box() {
        entry.hide();
      }
    
      public override void add(Widget widget) {
        if (widget != box) {
          box.pack_start(widget, true, true);
        } else {
          base.add(widget);
        }
      }
    
      public CustomWindow ()
      {
        box = new Box(Orientation.VERTICAL, 0);
        entry = new Entry();
        box.pack_start (entry, false, true);
        box.show();
    
        add(box);
    
        key_release_event.connect((event) => {
          search_updated(entry.text);
          return false;
        });
    
        key_press_event.connect((event) => {
          if (!entry.get_visible()) {
            show_search_box();
          }
          return false;
        });
      }
    }
  3. Use this one for the main class:
    using GLib;
    using Gtk;
    
    public class Main {
      static int main (string[] args)
      {
        Gtk.init (ref args);
        var window = new CustomWindow();
        var label = new Label("This is a text");
    
        window.add(label);
        window.resize(400,400);
        window.search_updated.connect((value) => {
          label.set_text("Searching for keyword " + value);
        });
    
        label.show();
        window.show();
        Gtk.main ();
    
        return 0;
      }
    }
  4. Then, modify the Makefile.am file to include both the implementation class and the main class.
    custom_composite_SOURCES = \
      custom_composite.vala main.vala config.vapi
  5. Next, let's create Makefile.am for the tests:
    AM_CPPFLAGS = \
      -DPACKAGE_LOCALE_DIR=\""$(localedir)"\" \
      -DPACKAGE_SRC_DIR=\""$(srcdir)"\" \
      -DPACKAGE_DATA_DIR=\""$(pkgdatadir)"\" \
      $(CUSTOM_COMPOSITE_CFLAGS)
    
    AM_CFLAGS =\
       -Wall\
       -g
    
    TESTS=test_custom_window
    check_PROGRAMS = test_custom_window
    
    test_custom_window_SOURCES = \
      ../src/custom_composite.vala test_custom_window.vala
    
    test_custom_window_VALAFLAGS =  \
      --pkg gtk+-3.0
    
    test_custom_window_LDFLAGS = \
      -Wl,--export-dynamic
    
    test_custom_window_LDADD = $(CUSTOM_COMPOSITE_LIBS)
  6. Create a new file called test_custom_window.vala inside tests and use this code:
    using Gtk;
    
    public class TestCustomWindow {
    
      static void process_events()
      {
        while (Gtk.events_pending ()) {
          Gtk.main_iteration_do(true);
        }
      }
    
      static void test_initial_child ()
      {
        var window = new CustomWindow();
        var child = window.get_child () as Box;
        window.show_now();
    
        assert (child != null);
        window.destroy ();
      }
    
      static void test_child_visibility ()
      {
        var window = new CustomWindow();
        var child = window.get_child () as Box;
        window.show_now();
    
        var entry_is_found = false;
        var children = child.get_children ();
        if (children != null && children.nth(0) != null) {
    
          var entry = children.nth_data(0) as Entry;
    
          assert (entry != null);
          assert (entry.visible == false);
    
          Gdk.test_simulate_key (window.get_window (), 1, 1, Gdk.Key.a, 0, Gdk.EventType.KEY_PRESS);
          Gdk.test_simulate_key (window.get_window (), 1, 1, Gdk.Key.a, 0, Gdk.EventType.KEY_RELEASE);
    
          process_events (); // Process events
    
          assert (entry.visible == true);
    
          entry_is_found = true;
        }
    
        assert (entry_is_found);
        window.destroy ();
      }
    
      static void test_search_updated ()
      {
        var window = new CustomWindow();
        window.show_now();
        var search_updated_was_emitted = false;
        var search_updated_was_correct = false;
    
        window.search_updated.connect ((text) => {
          search_updated_was_emitted = true;
          if (text == "a") {
            search_updated_was_correct = true;
          }
        });
    
        Gdk.test_simulate_key (window.get_window (), 1, 1, Gdk.Key.a, 0, Gdk.EventType.KEY_PRESS);
        Gdk.test_simulate_key (window.get_window (), 1, 1, Gdk.Key.a, 0, Gdk.EventType.KEY_RELEASE);
    
        process_events (); // process events
    
        assert (search_updated_was_emitted);
        assert (search_updated_was_correct);
        window.destroy ();
      }
    
      static int main (string[] args)
      {
        Gtk.test_init (ref args);
    
        Test.add_func ("/test-search-updated", test_search_updated);
        Test.add_func ("/test-initial-child", test_initial_child);
        Test.add_func ("/test-child-visibility", test_child_visibility);
    
    
        Idle.add (() => {
          Test.run ();
          Gtk.main_quit ();
          return true;
        });
    
        Gtk.main ();
    
        return 0;
      }
    }
  7. Rebuild the project by using this command:
    ./autogen.sh
    
  8. Set up the environment for the GUI initialization by issuing the following command:
    export DISPLAY=:0

    This only needs to be done once when we first run any GUI tests. This is needed when you run the test in the terminal console. If you run the test directly inside GNOME, you don't need this.

  9. On running the test you will see a window flashing for a moment before the test ends. Type the following command in the shell to run the test:
    make check
    
  10. Make sure all the tests are passed successfully.
    /test-search-updated: OK
    /test-initial-child: OK
    /test-child-visibility: OK
    PASS: test_custom_window
    =============
    1 test passed
    =============
    

Now we just directly concentrate on how the unit test is done.

Let's see what the CustomWindow class does:

This is the base of our strategy to create unit tests.

For our first test, we check whether the constructor creates the Box object as the placeholder for the Entry and other widgets correctly.

Here, we simply check whether the constructor has the Box object. This test is a straightforward test.

In the next test we check the visibility of the text entry. Initially, the text entry must be hidden, and after we press any key, the entry must be visible.

Here, we call show_now in order to display the window immediately. If we use show, the showing may be deferred until other events are processed.

Next, we try to get the text entry. The entry is the first child of the window, so we use the nth_data method and pass 0 to denote the element at index 0.

We use Entry to cast the returned widget as an Entry widget. By using this cast, it will return null if the widget returned is not an Entry widget. Here, we check whether the entry variable is really a type of an Entry widget by comparing it with the null value. If it succeeds, we continue checking whether the visibility is set to false because we want to make sure that the initial visibility for the entry must be false.

Then we simulate a key press followed with a key release by using test_simulate_key from the Gdk class. We send these events to the window object. The function requires Gdk.Window, so we use get_window to pass Gdk.Window from the window object. The usage of show_now for showing the window object is essential here, because if we use show and the showing is not yet performed, then the get_window function will return null and our test will fail.

We can use any location of the press and release events. Here, we use the (1,1) coordinate. The key we send is the a key, so we use Gdk.Key.a without any other key modifier. We send the press event first and then follow it with a release event.

Just after we send the key, we need to process the pending events by calling the process_events function. Then after that, we can check the visibility. Without calling process_events, the key events may not be processed by the system so the visibility of the entry object has not changed yet. We then destroy the window afterwards.

Here is what the process_events function does:

It basically processes any pending events in the queue. When it finds them, it simply executes the main loop by calling main_iteration_do and we allow GTK+ to complete the operation even when it is blocking by providing the true argument.

Next, we check whether the signal is emitted properly with the correct value.

Here, we connect the search_updated signal. We record the success of the handler with a local variable. We check whether the text from the signal is a, which is the key event that we send into the window.

After we send the key events, we call process_events. This is to make sure that our signal handler is called so that we can assert the result.

In our main function, we first call the Gtk.test_init function. This is to set up the environment to be suitable for testing. Next, we add the test paths.

We still need Gtk.main to get the GTK+ system up and running. When we run the test outside GNOME, we need to run the X11 server and set up the environment so that the test can run. We do this by setting the DISPLAY environment variable to our X11 display number. Typically the value is :0. This is a bit of a cumbersome setup especially if we do the test remotely.

However, once it is running, we need to ensure that our tests are run. To do this, we need to set up an idle handler. Inside the handler we simply run the test and then exit the GTK+ system.

In our previous test, we only checked for the existence of the Box object inside the window object. We missed another essential test to check:

Figure it out and let's create some tests!