Let's take our old custom_composite
Vala project and put a unit test in it.
tests
directory and include it in configure.ac
and Makefile.am
in the top directory.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; }); } }
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; } }
Makefile.am
file to include both the implementation class and the main class.custom_composite_SOURCES = \ custom_composite.vala main.vala config.vapi
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)
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; } }
./autogen.sh
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.
make check
/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:
search_updated
signal whenever the user presses a key in the windowThis 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.
static void test_initial_child () { var window = new CustomWindow(); var child = window.get_child () as Box; window.show_now(); assert (child != null); window.destroy (); }
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.
static void test_child_visibility () { var window = new CustomWindow(); var child = window.get_child () as Box; window.show_now();
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.
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;
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
.
assert (entry != null); assert (entry.visible == false);
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
.
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);
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.
process_events (); // Process events assert (entry.visible == true); entry_is_found = true; } assert (entry_is_found); window.destroy (); }
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:
static void process_events() { while (Gtk.events_pending ()) { Gtk.main_iteration_do(true); } }
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.
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; } });
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.
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
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.
assert (search_updated_was_emitted); assert (search_updated_was_correct); window.destroy ();
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.
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);
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.
Idle.add (() => { Test.run (); Gtk.main_quit (); return true; }); Gtk.main ();
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:
Entry
is really createdEntry
can coexist when we add a new widget into the window
object.Figure it out and let's create some tests!