[geary/wip/794174-conversation-monitor-max-cpu: 13/14] Make new unit test methods and the mock object a bit more useful.



commit 6392364f192f79c7f4a0110fd9702d92fcb0f2ca
Author: Michael James Gratton <mike vee net>
Date:   Fri Mar 9 12:03:07 2018 +1100

    Make new unit test methods and the mock object a bit more useful.
    
    * test/mock-object.vala: Make ExpectedCall object public, use a
      style API to add additional expectations to it. Make MockObject
      always throw on unexpected call, so ActualCall and related
      machinery can be removed.
    
    * test/test-case.vala: Add some additional assert methods, allow
      null actual values where it makes sense. Add TestCase
      wait_for_call and wait_for_signal for better async testing.

 test/mock-object.vala |  176 +++++++++++++++++++++++++++++--------------------
 test/test-case.vala   |   86 +++++++++++++++++++++++-
 2 files changed, 187 insertions(+), 75 deletions(-)
---
diff --git a/test/mock-object.vala b/test/mock-object.vala
index 2902ce5..59b4b56 100644
--- a/test/mock-object.vala
+++ b/test/mock-object.vala
@@ -44,98 +44,111 @@ private class IntArgument : Object, Argument {
 
 }
 
+/**
+ * Represents an expected method call on a mock object.
+ *
+ * An instance of this object is returned when calling {@link
+ * Mock.Object.expect_call}, and may be used to further specify
+ * expectations, such that the mock method should throw a specific
+ * error or return a specific value or object.
+ */
+public class ExpectedCall : Object {
 
-public interface MockObject {
 
+    public string name { get; private set; }
+    internal Object[]? args;
+    public Error? throw_error { get; private set; default = null; }
+    public Object? return_object { get; private set; default = null; }
+    public Variant? return_value { get; private set; default = null; }
+    public bool was_called { get; private set; default = false; }
 
-    public static Object box_arg<T>(T value) {
-        return new BoxArgument<T>(value);
-    }
 
-    public static Object int_arg(int value) {
-        return new IntArgument(value);
+    internal ExpectedCall(string name, Object[]? args) {
+        this.name = name;
+        this.args = args;
     }
 
-    public class ExpectedCall : Object {
+    public ExpectedCall returns_object(Object value) {
+        this.return_object = value;
+        return this;
+    }
 
+    public ExpectedCall returns_boolean(bool value) {
+        this.return_value = new GLib.Variant.boolean(value);
+        return this;
+    }
 
-        public string name { get; private set; }
-        internal Object[]? args;
-        public Error? throw_error { get; private set; default = null; }
-        public Object? return_object { get; private set; default = null; }
-        public Variant? return_value { get; private set; default = null; }
+    public ExpectedCall @throws(Error err) {
+        this.throw_error = err;
+        return this;
+    }
 
+    internal void called() {
+        this.was_called = true;
+    }
 
-        internal ExpectedCall(string name, Object[]? args) {
-            this.name = name;
-            this.args = args;
-        }
+}
 
-        public ExpectedCall returns_object(Object value) {
-            this.return_object = value;
-            return this;
-        }
 
-        public ExpectedCall returns_boolean(bool value) {
-            this.return_value = new GLib.Variant.boolean(value);
-            return this;
-        }
+/**
+ * Denotes a class that is injected into code being tested.
+ *
+ * Mock objects are unit testing fixtures that are used to provide
+ * instances of specific classes or interfaces which are required by
+ * the code being tested. For example, if an object being tested
+ * requires certain objects to be passed in via its constructor or as
+ * arguments of method calls and uses these to implement its
+ * behaviour, mock objects that fulfil these requirements can be used.
+ *
+ * Mock objects provide a means of both ensuring code being tested
+ * makes expected method calls with expected arguments on its
+ * dependencies, and a means of orchestrating the return value and
+ * exceptions raised when these methods are called, if any.
+ *
+ * To specify a specific method should be called on a mock object,
+ * call {@link expect_call} with the name of the method and optionally
+ * the arguments that are expected. The returned {@link ExpectedCall}
+ * object can be used to specify any exception or return values for
+ * the method. After executing the code being tested, call {@link
+ * assert_expectations} to ensure that the actual calls made matched
+ * those expected.
+ */
+public interface MockObject {
 
-        public ExpectedCall @throws(Error err) {
-            this.throw_error = err;
-            return this;
-        }
 
+    public static Object box_arg<T>(T value) {
+        return new BoxArgument<T>(value);
     }
 
-
-    protected struct ActualCall {
-        string name;
-        Object[] args;
+    public static Object int_arg(int value) {
+        return new IntArgument(value);
     }
 
-
-    protected abstract Gee.List<ExpectedCall> expected { get; set; }
-    protected abstract Gee.List<ActualCall?> actual { get; set; }
+    protected abstract Gee.Queue<ExpectedCall> expected { get; set; }
 
 
     public ExpectedCall expect_call(string name, Object[]? args = null) {
         ExpectedCall expected = new ExpectedCall(name, args);
-        this.expected.add(expected);
+        this.expected.offer(expected);
         return expected;
     }
 
-    public void assert_expectations() throws Error{
-        int calls = 0;
-        foreach (ExpectedCall expected in this.expected) {
-            string context = "Call #%d, %s".printf(calls, expected.name);
-            ActualCall? actual = null;
-            if (calls < this.actual.size) {
-                actual = this.actual.get(calls++);
-            }
-
-            assert_true(actual != null, context + " not made");
-            assert_string(expected.name, actual.name, context);
-            if (expected.args != null) {
-                assert_args(expected.args, actual.args, context);
-            }
-        }
+    public void assert_expectations() throws Error {
+        assert_true(this.expected.is_empty,
+                    "%d expected calls not made".printf(this.expected.size));
+        reset_expectations();
+    }
 
-        assert_int(this.expected.size, this.actual.size,
-                   "Number of calls");
+    public void reset_expectations() {
+        this.expected.clear();
     }
 
     protected bool boolean_call(string name, Object[] args, bool default_return)
         throws Error {
-        ExpectedCall? expected = get_next_expected();
-        this.actual.add({ name, args });
-
-        if (expected.throw_error != null) {
-            throw expected.throw_error;
-        }
+        ExpectedCall? expected = call_made(name, args);
 
         bool return_value = default_return;
-        if (expected != null && expected.return_value != null) {
+        if (expected.return_value != null) {
             return_value = expected.return_value.get_boolean();
         }
         return return_value;
@@ -143,26 +156,45 @@ public interface MockObject {
 
     protected R object_call<R>(string name, Object[] args, R default_return)
         throws Error {
-        ExpectedCall? expected = get_next_expected();
-        this.actual.add({ name, args });
-
-        if (expected.throw_error != null) {
-            throw expected.throw_error;
-        }
+        ExpectedCall? expected = call_made(name, args);
 
         R? return_object = default_return;
-        if (expected != null && expected.return_object != null) {
+        if (expected.return_object != null) {
             return_object = (R) expected.return_object;
         }
         return return_object;
     }
 
-    private inline ExpectedCall? get_next_expected() {
-        ExpectedCall? next = null;
-        if (this.expected.size > this.actual.size) {
-            next = this.expected.get(this.actual.size);
+    protected R object_or_throw_call<R>(string name, Object[] args, Error default_error)
+        throws Error {
+        ExpectedCall? expected = call_made(name, args);
+
+        if (expected.return_object != null) {
+            throw default_error;
+        }
+        return expected.return_object;
+    }
+
+    protected void void_call(string name, Object[] args) throws Error {
+        call_made(name, args);
+    }
+
+    private ExpectedCall? call_made(string name, Object[] args) throws Error {
+        assert_false(this.expected.is_empty, "Unexpected call: %s".printf(name));
+
+        ExpectedCall expected = this.expected.poll();
+        assert_string(expected.name, name, "Unexpected call");
+        if (expected.args != null) {
+            assert_args(expected.args, args, "Call %s".printf(name));
         }
-        return next;
+
+        expected.called();
+
+        if (expected.throw_error != null) {
+            throw expected.throw_error;
+        }
+
+        return expected;
     }
 
     private void assert_args(Object[]? expected_args, Object[]? actual_args, string context)
diff --git a/test/test-case.vala b/test/test-case.vala
index 7c85a1f..78502d2 100644
--- a/test/test-case.vala
+++ b/test/test-case.vala
@@ -22,15 +22,31 @@
  */
 
 
-public void assert_equal(Object expected, Object actual, string? context = null)
+public void assert_null(Object? actual, string? context = null)
+    throws Error {
+    if (actual != null) {
+        print_assert(context ?? "Object is non-null", null);
+        assert_not_reached();
+    }
+}
+
+public void assert_non_null(Object? actual, string? context = null)
+    throws Error {
+    if (actual == null) {
+        print_assert(context ?? "Object is null", null);
+        assert_not_reached();
+    }
+}
+
+public void assert_equal(Object expected, Object? actual, string? context = null)
     throws Error {
     if (expected != actual) {
-        print_assert(context ?? "Objects not equal", null);
+        print_assert(context ?? "Objects are not equal", null);
         assert_not_reached();
     }
 }
 
-public void assert_string(string expected, string actual, string? context = null)
+public void assert_string(string expected, string? actual, string? context = null)
     throws Error {
     if (expected != actual) {
         string a = expected;
@@ -62,6 +78,14 @@ public void assert_true(bool condition, string? context = null)
     }
 }
 
+public void assert_false(bool condition, string? context = null)
+    throws Error {
+    if (condition) {
+        print_assert(context ?? "Expected false", null);
+        assert_not_reached();
+    }
+}
+
 public void assert_error(Error expected, Error? actual, string? context = null) {
     bool failed = false;
     if (actual == null) {
@@ -118,6 +142,17 @@ private inline void print_assert(string message, string? context) {
 
 public abstract class TestCase : Object {
 
+
+    private class SignalWaiter : Object {
+
+        public bool was_fired = false;
+
+        public void @callback(Object source) {
+            was_fired = true;
+        }
+    }
+
+
     protected MainContext main_loop = MainContext.default();
 
        private GLib.TestSuite suite;
@@ -170,6 +205,51 @@ public abstract class TestCase : Object {
         return result;
     }
 
+    /**
+     * Waits for a mock object's call to be completed.
+     *
+     * This method busy waits on the test's main loop until either
+     * until {@link ExpectedCall.was_called} is true, or until the
+     * given timeout in seconds has occurred.
+     *
+     * Returns //true// if the call was made, or //false// if the
+     * timeout was reached.
+     */
+    protected bool wait_for_call(ExpectedCall call, double timeout = 1.0) {
+        GLib.Timer timer = new GLib.Timer();
+        timer.start();
+        while (!call.was_called && timer.elapsed() < timeout) {
+            this.main_loop.iteration(false);
+        }
+        return call.was_called;
+    }
+
+    /**
+     * Waits for an object's signal to be fired.
+     *
+     * This method busy waits on the test's main loop until either
+     * until the object emits the named signal, or until the given
+     * timeout in seconds has occurred.
+     *
+     * Returns //true// if the signal was fired, or //false// if the
+     * timeout was reached.
+     */
+    protected bool wait_for_signal(Object source, string name, double timeout = 0.5) {
+        SignalWaiter handler = new SignalWaiter();
+        ulong id = GLib.Signal.connect_swapped(
+            source, name, (GLib.Callback) handler.callback, handler
+        );
+
+        GLib.Timer timer = new GLib.Timer();
+        timer.start();
+        while (!handler.was_fired && timer.elapsed() < timeout) {
+            this.main_loop.iteration(false);
+        }
+
+        source.disconnect(id);
+        return handler.was_fired;
+    }
+
        private class Adaptor {
 
                public string name { get; private set; }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]