Permalink
Browse files

only records are considered matchers - API breakage

  • Loading branch information...
1 parent 4e8d990 commit f768562cc865d9b95caaf001ffb1f3a7f9008c65 @hyperthunk committed Nov 16, 2012
View
@@ -1,19 +1,23 @@
Hamcrest Erlang [![travis](https://secure.travis-ci.org/hyperthunk/hamcrest-erlang.png)](http://travis-ci.org/hyperthunk/hamcrest-erlang)
=============================
-This is an implementation of Hamcrest for the [Erlang programming language](http://www.erlang.org/). This
-tutorial has largely been cribbed from the hamcrest-python version, upon which the API is also based.
-
-Hamcrest is a framework for writing matcher objects allowing 'match' rules to be defined declaratively.
-There are a number of situations where matchers are invaluable, such as UI validation, or data filtering,
-but it is in the area of writing flexible tests that matchers are most commonly used.
-
-This tutorial shows you how to use Hamcrest for unit testing. When writing tests it is sometimes difficult
-to get the balance right between over specifying the test (and making it brittle to changes), and not
-specifying enough (making the test less valuable since it continues to pass even when the thing being tested
-is broken). Having a tool that allows you to pick out precisely the aspect under test and describe the values
-it should have, to a controlled level of precision, helps greatly in writing tests that are "just right".
-Such tests fail when the behaviour of the aspect under test deviates from the expected behaviour, yet continue
+This is an implementation of Hamcrest for the
+[Erlang programming language](http://www.erlang.org/). This tutorial has largely
+been cribbed from the hamcrest-python version, upon which the API is also based.
+
+Hamcrest is a framework for writing matcher objects allowing 'match' rules to be
+defined declaratively. There are a number of situations where matchers are
+invaluable, such as UI validation, or data filtering, but it is in the area of
+writing flexible tests that matchers are most commonly used.
+
+This tutorial shows you how to use Hamcrest for unit testing. When writing tests
+it is sometimes difficult to get the balance right between over specifying the
+test (and making it brittle to changes), and not specifying enough (making the
+test less valuable since it continues to pass even when the thing being tested is
+broken). Having a tool that allows you to pick out precisely the aspect under
+test and describe the values it should have, to a controlled level of precision,
+helps greatly in writing tests that are "just right". Such tests fail when the
+behaviour of the aspect under test deviates from the expected behaviour, yet continue
to pass when minor, unrelated changes to the behaviour are made.
Building Hamcrest-Erlang
@@ -26,13 +30,15 @@ generate the main header file:
$ ./rebar clean && ./rebar compile
-Please note that currently there is a bug in the build plugins that requires these steps to be executed separately.
+Please note that currently there is a bug in the build plugins that
+requires these steps to be executed separately.
My first Hamcrest test
------------------------
-We'll start be writing a very simple EUnit test, but instead of using EUnit's ?assertEqual macro, we use
-Hamcrest's assert_that construct and the standard set of matchers:
+We'll start be writing a very simple EUnit test, but instead of
+using EUnit's ?assertEqual macro, we use Hamcrest's `assert_that`
+construct and the standard set of matchers:
```erlang
-module(demo1_tests).
@@ -46,10 +52,12 @@ using_assert_that_test() ->
assert_that(10, is(greater_than(2))). %% returns true
```
-The assert_that function is a stylised sentence for making a test assertion. In this example, the subject of the
-assertion is the number 10 that is the first method parameter. The second method parameter is a matcher
-for numbers, here a matcher that checks one number is greater than another using the standard > operator.
-If the test passes (as it will in this case) then assert_that returns the atom 'true' by default.
+The assert_that function is a stylised sentence for making a test
+assertion. In this example, the subject of the assertion is the number
+10 that is the first method parameter. The second method parameter is a
+matcher for numbers, here a matcher that checks one number is greater
+than another using the standard > operator. If the test passes (as it
+will in this case) then assert_that returns the atom 'true' by default.
Standard Matchers
------------------
@@ -83,22 +91,27 @@ The match_mfa/3 function is a building block for creating custom matchers withou
Using hamcrest assertions
----------------------------
-The `assert_that/2` and `assert_that/3` functions are the workhorses of the library. Each function takes an *actual* value and a
-match specification (more on this later) and apply the match function to the *actual* input value. If the match function evaluates
-to `false`, the descriptor on the match specification is used to construct and error message and an exception is thrown.
+The `assert_that/2` and `assert_that/3` functions are the workhorses of
+the library. Each function takes an *actual* value and a match specification
+(more on this later) and apply the match function to the *actual* input value.
+If the match function evaluates to `false`, the descriptor on the match
+specification is used to construct and error message and an exception is thrown.
-The two and three argument `?assertThat` macros work in exactly the same way, except they borrow their error reporting mechanism
-from eunit and provide information about the line number and expectation failure.
+The two and three argument `?assertThat` macros work in exactly the same way,
+except they borrow their error reporting mechanism from eunit and provide
+information about the line number and expectation failure.
Writing custom matchers
----------------------------
-Hamcrest comes bundled with a few useful matchers, but you'll probably find that you need to create your own from
-time to time. This commonly occurs when you find a fragment of code that tests the same set of properties over and
-over again (and in different tests), and you want to bundle the fragment into a single assertion. By writing your
-own matcher you'll eliminate code duplication and make your tests more readable!
+Hamcrest comes bundled with a few useful matchers, but you'll
+probably find that you need to create your own from time to time.
+This commonly occurs when you find a fragment of code that tests
+the same set of properties over and over again (and in different tests),
+and you want to bundle the fragment into a single assertion.
-Let's write our own matcher for testing if a string is comprised of only digits. This is the test we want to write:
+Let's write our own matcher for testing if a string is comprised
+of only digits. This is the test we want to write:
```erlang
string_is_only_digits_test() ->
@@ -110,6 +123,8 @@ And here's the implementation:
```erlang
-module(custom_matchers).
+-include("../include/hamcrest.hrl").
+
-export([only_digits/0]).
only_digits() ->
@@ -122,27 +137,44 @@ only_digits(X) ->
end.
```
-The zero arity factory function we exported is responsible for creating the matcher fun, which should take 1 argument
-and return the atom 'true' if it succeeds, otherwise 'false'. Although returning a fun is a simple enough way to define
-a matcher, there is a another way that allows you to specify the expected versus actual input, the matcher fun and a
-textual description that will be evaluated by hamcrest:assert_that/2 in case of match failures. Here's the only_digits/0
-example rewritten using the alternative API:
+The zero arity factory function we exported is responsible
+for creating the matcher fun, which should take 1 argument
+and return the atom 'true' if it succeeds, otherwise 'false'.
+The matcher fun must however be wrapped in a record, which also
+allows you to specify the expected versus actual input, and a textual
+description that will be evaluated by hamcrest:assert_that/2 in
+case of match failures. The `?MATCHER` macro simply takes the
+match function, description and expected input domain and generates
+an instance of this record.
+
+The desc field can contain a format string to be evaluated against
+the expected and actual values, or a function that returns a textual
+description instead. In the former case, the format string will be
+evaluated as `lists:flatten(io_lib:format(Desc, [Expected, Actual]))`
+and the hamcrest:message/4 function exists to provide a simple wrapper
+around that feature, allowing you to specify the whether the input is
+a string, an error tuple or another (arbitrary) term. It evaluates its
+arguments to produce a string that looks something like
+`expected a <TYPE> <DESC> <EXPECTED>, but was <ACTUAL>` when called.
+
+Re-using custom matchers
+------------------------
-```erlang
--module(custom_matchers).
+Whilst `only_digits/1` is a useful little function, we can improve on
+this by generalising it to handle any supplied regex compatible with
+the 're' module. Doing this makes the function re-usable in other contexts,
+and in fact there is a matcher for this already in the base library!
--export([only_digits/0]).
-
-only_digits() ->
- matches_regex("^[\\d]*$").
+The `matches_regex` matcher is defined in `hamcrest_matchers` as
+```erlang
matches_regex(Rx) ->
#'hamcrest.matchspec'{
matcher =
fun(X) ->
case re:run(X, Rx) of
{match,_} -> true;
- _ -> false
+ _ -> false
end
end,
desc = fun(Expected,Actual) ->
@@ -152,42 +184,34 @@ matches_regex(Rx) ->
}.
```
-First of all, note that we're now calling the generic matches_regex/1 function which will operate over any supplied regex
-compatible with the 're' module. This function is returning an instance of the 'hamcrest.matchspec' record, defined in
-include/hamcrest.hrl. The matcher field points to the actual match function (which needs to return true or false as before).
-The expected field contains the 'expected' value, which in this case is the supplied regex. The desc field can contain a
-format string to be evaluated against the expected and actual values, or a function that returns a textual description instead.
-In the former case, the format string will be evaluated as `lists:flatten(io_lib:format(Desc, [Expected, Actual]))` and the
-hamcrest:message/4 function exists to provide a simple wrapper around that feature, allowing you to specify the whether the
-input is a string, an error tuple or another (arbitrary) term. It evaluates its arguments to produce a string that looks
-something like `expected a <TYPE> <DESC> <EXPECTED>, but was <ACTUAL>` when called.
-
-If you find the record syntax a little too verbose for your liking, there is a macro defined which cuts down on the noise somewhat:
+Given a definition such as this, our `is_digits` matcher becomes much simpler,
+as would `is_alpha_numeric` and many other string matches that regular
+expressions would make easy work of.
```erlang
-will_fail() ->
- Matcher =
- fun(F) ->
- try F() of
- _ -> false
- catch _:_ -> true
- end
- end,
- ?MATCHER(Matcher, expected_fail, {oneof, {exit,error,exception}}).
+-module(custom_matchers).
+
+-include_lib("hamcrest/include/hamcrest.hrl").
+-export([only_digits/0]).
+
+only_digits() ->
+ matches_regex("^[\\d]*$").
```
-Another way to construct custom matchers is to use the match_mfa/3 function, which takes a module, function and its initial
-arguments and matches if adding the match input to the argument list and calling `apply(M,F,A)` evaluates to true. This takes
-away the headache of having to write the matchspec yourself, at the expense of a slightly less specific description provided
-by assert_that if the match fails. Here's an example usage taken from the tests and another taken from the core API itself:
+Notice that this time we created the record explicitly instead of using
+the `?MATCHER` macro. Yet another way to construct custom matchers is to
+use the match_mfa/3 function, which takes a module, function and its
+initial arguments and matches if adding the match input to the argument
+list and calling `apply(M,F,A)` evaluates to true. This takes away the headache
+of having to write the matchspec yourself, at the expense of a slightly less
+specific description provided by assert_that if the match fails. Here's an
+example usage taken from the tests and another taken from the core API itself:
```erlang
isalive() ->
match_mfa(erlang, is_process_alive, []).
some_test(_) ->
- IsMemberOf = fun(L) ->
- match_mfa(lists, member, [L])
- end,
- assert_that(hd(X), IsMemberOf(X))
+ IsMemberOf = match_mfa(lists, member, [[a,b,c]]),
+ assert_that(a, IsMember).
```
@@ -29,15 +29,22 @@
-record('hamcrest.matchspec', {
matcher = undefined :: fun((term()) -> boolean()),
expected = undefined :: term(),
- desc = "" :: string() | atom() | fun((term(), term()) -> string())
+ desc = "" :: term()
}).
+-type(matchfun(A) :: fun((A) -> boolean())).
+-opaque(matchspec(A) :: {'hamcrest.matchspec', matchfun(A), term(), term()}).
@horkhe
horkhe Nov 17, 2012 Contributor

@hyperthunk Hey Tim, when I try to build PLT that includes hamcrest the following error appears:

dialyzer: Analysis failed with error:
hamcrest_matchers.erl:283: Polymorphic opaque types not supported yet

You probably better fix that since it might break dialyzer checks of other projects that depend on hamcrest

+
+-type(container_t() :: list() | set() | gb_set()).
+
+-export_type([matchspec/1]).
+
-define(MATCHER(MatchFun, Expected, Desc),
- #'hamcrest.matchspec'{
- matcher=MatchFun,
- expected=Expected,
- desc=Desc
- }).
+ #'hamcrest.matchspec'{
+ matcher=MatchFun,
+ expected=Expected,
+ desc=Desc
+ }).
-define(HECKLE(M,F,A),
.application:set_env(hamcrest, heckle, [M, F, A])).
@@ -74,5 +74,5 @@ end).
end)())).
-endif.
--import(hamcrest, [assert_that/2, assert_that/3]).
+-import(hamcrest, [assert_that/2, assert_that/3, match/2, match/3]).
-import(hamcrest_matchers, [{{ imports }}]).
Oops, something went wrong.

0 comments on commit f768562

Please sign in to comment.