From 769427de5f28751134ef9684398b1ad780113515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Thu, 24 Dec 2015 14:15:35 +0100 Subject: Add EUnit tests and documentation Also includes a fix for multi-application repositories. --- core/test.mk | 6 ++ doc/src/guide/eunit.asciidoc | 113 ++++++++++++++++++++++++- plugins/eunit.mk | 28 +++++-- test/Makefile | 47 +---------- test/plugin_eunit.mk | 195 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 336 insertions(+), 53 deletions(-) create mode 100644 test/plugin_eunit.mk diff --git a/core/test.mk b/core/test.mk index d7b0bfe..12ff208 100644 --- a/core/test.mk +++ b/core/test.mk @@ -29,6 +29,11 @@ test-dir: $(call core_find,$(TEST_DIR)/,*.erl) -pa ebin/ endif +ifeq ($(wildcard src),) +test-build:: ERLC_OPTS=$(TEST_ERLC_OPTS) +test-build:: clean deps test-deps + $(verbose) $(MAKE) --no-print-directory test-dir ERLC_OPTS="$(TEST_ERLC_OPTS)" +else ifeq ($(wildcard ebin/test),) test-build:: ERLC_OPTS=$(TEST_ERLC_OPTS) test-build:: clean deps test-deps $(PROJECT).d @@ -46,3 +51,4 @@ clean-test-dir: ifneq ($(wildcard $(TEST_DIR)/*.beam),) $(gen_verbose) rm -f $(TEST_DIR)/*.beam endif +endif diff --git a/doc/src/guide/eunit.asciidoc b/doc/src/guide/eunit.asciidoc index 1a16776..0295726 100644 --- a/doc/src/guide/eunit.asciidoc +++ b/doc/src/guide/eunit.asciidoc @@ -1,5 +1,114 @@ == EUnit -// @todo Write it. +EUnit is the tool of choice for unit testing. Erlang.mk +automates a few things on top of EUnit, including the +discovery and running of unit tests. -Placeholder chapter. +=== Writing tests + +The http://www.erlang.org/doc/apps/eunit/chapter.html[EUnit user guide] +is the best place to learn how to write tests. Of note is +that all functions ending with `_test` or `_test_` will be +picked up as EUnit test cases. + +Erlang.mk will automatically pick up tests found in any of +the Erlang modules of your application. It will also pick up +tests located in the '$(TEST_DIR)' directory, which defaults +to 'test/'. + +It is generally a good practice to hide test code from +the code you ship to production. With Erlang.mk, you can +do this thanks to the `TEST` macro. It is only defined +when running tests: + +[source,erlang] +---- +-ifdef(TEST). + +%% Insert tests here. + +-endif. +---- + +Be careful, however, if you include the EUnit header file, +as it also defines the `TEST` macro. Make sure to only include +it inside an `ifdef` block, otherwise tests will always be +compiled. + +[source,erlang] +---- +-ifdef(TEST). + +-include_lib(\"eunit/include/eunit.hrl\"). + +%% Insert tests here. + +-endif. +---- + +Erlang.mk will automatically recompile your code when you +perform a normal build after running tests, and vice versa. + +=== Configuration + +The `EUNIT_OPTS` variable allows you to specify additional +EUnit options. Options are documented in the +http://www.erlang.org/doc/man/eunit.html#test-2[EUnit manual]. +At the time of writing, the only available option is `verbose`: + +[source,make] +EUNIT_OPTS = verbose + +=== Usage + +To run all tests (including EUnit): + +[source,bash] +$ make tests + +To run all tests and static checks (including EUnit): + +[source,bash] +$ make check + +You can also run EUnit separately: + +[source,bash] +$ make eunit + +EUnit will be quiet by default, only outputting errors. +You can easily make it verbose for a single invocation: + +[source,bash] +$ make eunit EUNIT_OPTS=verbose + +Erlang.mk allows you to run all tests from a specific +module, or a specific test case from that module, using +the variable `t`. + +For example, to run all tests from the `cow_http_hd` +module (instead of all tests from the entire project), +one could write: + +[source,bash] +$ make eunit t=cow_http_hd + +Similarly, to run a specific test case: + +[source,bash] +$ make eunit t=cow_http_hd:parse_accept_test_ + +To do the same against a multi-application repository, +you can use the `-C` option: + +[source,bash] +$ make -C apps/my_app eunit t=my_module:hello_test + +Note that this also applies to dependencies. From Cowboy, +you can run the following directly: + +[source,bash] +$ make -C deps/cowlib eunit t=cow_http_hd + +Finally, link:coverage.asciidoc[code coverage] is available, +but covered in its own chapter. diff --git a/plugins/eunit.mk b/plugins/eunit.mk index 2adf4a6..36de6e7 100644 --- a/plugins/eunit.mk +++ b/plugins/eunit.mk @@ -2,7 +2,7 @@ # Copyright (c) 2015, Loïc Hoguin # This file is contributed to erlang.mk and subject to the terms of the ISC License. -.PHONY: eunit +.PHONY: eunit apps-eunit # Configuration @@ -28,7 +28,7 @@ define eunit.erl _ -> ok end end, - case eunit:test([$(call comma_list,$(1))], [$(EUNIT_OPTS)]) of + case eunit:test($1, [$(EUNIT_OPTS)]) of ok -> ok; error -> halt(2) end, @@ -40,11 +40,27 @@ define eunit.erl halt() endef +EUNIT_PATHS = -pa $(TEST_DIR) $(DEPS_DIR)/*/ebin $(APPS_DIR)/*/ebin ebin + +ifdef t +ifeq (,$(findstring :,$(t))) +eunit: test-build + $(gen_verbose) $(call erlang,$(call eunit.erl,['$(t)']),$(EUNIT_PATHS)) +else +eunit: test-build + $(gen_verbose) $(call erlang,$(call eunit.erl,fun $(t)/0),$(EUNIT_PATHS)) +endif +else EUNIT_EBIN_MODS = $(notdir $(basename $(call core_find,ebin/,*.beam))) EUNIT_TEST_MODS = $(notdir $(basename $(call core_find,$(TEST_DIR)/,*.beam))) EUNIT_MODS = $(foreach mod,$(EUNIT_EBIN_MODS) $(filter-out \ - $(patsubst %,%_tests,$(EUNIT_EBIN_MODS)),$(EUNIT_TEST_MODS)),{module,'$(mod)'}) + $(patsubst %,%_tests,$(EUNIT_EBIN_MODS)),$(EUNIT_TEST_MODS)),'$(mod)') -eunit: test-build - $(gen_verbose) $(ERL) -pa $(TEST_DIR) $(DEPS_DIR)/*/ebin ebin \ - -eval "$(subst $(newline),,$(subst ",\",$(call eunit.erl,$(EUNIT_MODS))))" +eunit: test-build $(if $(IS_APP),,apps-eunit) + $(gen_verbose) $(call erlang,$(call eunit.erl,[$(call comma_list,$(EUNIT_MODS))]),$(EUNIT_PATHS)) + +ifneq ($(ALL_APPS_DIRS),) +apps-eunit: + $(verbose) for app in $(ALL_APPS_DIRS); do $(MAKE) -C $$app eunit IS_APP=1; done +endif +endif diff --git a/test/Makefile b/test/Makefile index d5a3a61..94dad9f 100644 --- a/test/Makefile +++ b/test/Makefile @@ -310,9 +310,9 @@ endef # The following tests are slowly being converted. # Do NOT use -j with legacy tests. -.PHONY: legacy clean-legacy ct eunit tests-cover docs +.PHONY: legacy clean-legacy ct tests-cover docs -legacy: clean-legacy ct eunit tests-cover docs pkgs +legacy: clean-legacy ct tests-cover docs pkgs clean-legacy: $t rm -rf app1 @@ -353,49 +353,6 @@ ct: app1 $t rm -rf app1/test $i "Test 'ct' passed." -eunit: app1 - $i "eunit: Testing the 'eunit' target." - $i "Running eunit test case inside module src/t.erl" - $t $(call create-module-t) - $t $(MAKE) -C app1 distclean $v - $t $(MAKE) -C app1 eunit $v - $i "Checking that the eunit test in module t." - $t echo t | cmp app1/test-eunit.log - - $t rm app1/test-eunit.log - $i "Running eunit tests in a separate directory." - $t mkdir -p app1/eunit - $t printf '%s\n' \ - '-module(t_tests).' \ - '-include_lib("eunit/include/eunit.hrl").' \ - 'succ_test() ->' \ - ' ?assertEqual(2, t:succ(1)),' \ - ' os:cmd("echo t_tests >> test-eunit.log").' \ - > app1/eunit/t_tests.erl - $t printf '%s\n' \ - '-module(x_tests).' \ - '-include_lib("eunit/include/eunit.hrl").' \ - 'succ_test() ->' \ - ' ?assertEqual(2, t:succ(1)),' \ - ' os:cmd("echo x_tests >> test-eunit.log").' \ - > app1/eunit/x_tests.erl - $t $(MAKE) -C app1 distclean TEST_DIR=eunit $v - $t $(MAKE) -C app1 eunit TEST_DIR=eunit $v - $i "Checking that '$(MAKE) eunit' didn't run the tests in t_tests twice, etc." - $t printf "%s\n" t t_tests x_tests | cmp app1/test-eunit.log - - $t rm app1/test-eunit.log - $i "Checking that '$(MAKE) eunit' returns non-zero for a failing test." - $t rm -f app1/eunit/* - $t printf "%s\n" \ - "-module(t_tests)." \ - '-include_lib("eunit/include/eunit.hrl").' \ - "succ_test() ->" \ - " ?assertEqual(42, t:succ(1))." \ - > app1/eunit/t_tests.erl - $t $(MAKE) -C app1 distclean TEST_DIR=eunit $v - $t ! $(MAKE) -C app1 eunit TEST_DIR=eunit $v - $t rm -rf app1/eunit app1/src/t.erl app1/test-eunit.log - $i "Test 'eunit' passed." - # TODO: do coverage for 'tests' instead of 'eunit ct' when triq is fixed tests-cover: app1 $i "tests-cover: Testing 'eunit' and 'ct' with COVER=1" diff --git a/test/plugin_eunit.mk b/test/plugin_eunit.mk new file mode 100644 index 0000000..26c0997 --- /dev/null +++ b/test/plugin_eunit.mk @@ -0,0 +1,195 @@ +# EUnit plugin. + +EUNIT_CASES = all apps-only check fun mod test-dir tests +EUNIT_TARGETS = $(addprefix eunit-,$(EUNIT_CASES)) +EUNIT_CLEAN_TARGETS = $(addprefix clean-,$(EUNIT_TARGETS)) + +.PHONY: eunit $(EUNIT_TARGETS) clean-eunit $(EUNIT_CLEAN_TARGETS) + +clean-eunit: $(EUNIT_CLEAN_TARGETS) + +$(EUNIT_CLEAN_TARGETS): + $t rm -rf $(APP_TO_CLEAN) + +eunit: $(EUNIT_TARGETS) + +eunit-all: build clean-eunit-all + + $i "Bootstrap a new OTP application named $(APP)" + $t mkdir $(APP)/ + $t cp ../erlang.mk $(APP)/ + $t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v + + $i "Check that EUnit detects no tests" + $t $(MAKE) -C $(APP) eunit | grep -q "There were no tests to run." + + $i "Generate a module containing EUnit tests" + $t printf "%s\n" \ + "-module($(APP))." \ + "-ifdef(TEST)." \ + "-include_lib(\"eunit/include/eunit.hrl\")." \ + "ok_test() -> ok." \ + "-endif." > $(APP)/src/$(APP).erl + + $i "Build the project cleanly" + $t $(MAKE) -C $(APP) clean $v + $t $(MAKE) -C $(APP) $v + + $i "Check that no EUnit test cases were exported" + $t $(ERL) -pa $(APP)/ebin -eval 'code:load_file($(APP)), false = erlang:function_exported($(APP), ok_test, 0), halt()' + + $i "Check that EUnit runs tests" + $t $(MAKE) -C $(APP) eunit | grep -q "Test passed." + + $i "Add a failing test to the module" + $t printf "%s\n" \ + "-ifdef(TEST)." \ + "bad_test() -> throw(fail)." \ + "-endif." >> $(APP)/src/$(APP).erl + + $i "Check that EUnit errors out" + $t ! $(MAKE) -C $(APP) eunit $v + +eunit-apps-only: build clean-eunit-apps-only + + $i "Create a multi application repository with no root application" + $t mkdir $(APP)/ + $t cp ../erlang.mk $(APP)/ + $t echo "include erlang.mk" > $(APP)/Makefile + + $i "Create a new application named my_app" + $t $(MAKE) -C $(APP) new-app in=my_app $v + + $i "Create a new library named my_lib" + $t $(MAKE) -C $(APP) new-lib in=my_lib $v + + $i "Check that EUnit detects no tests" + $t $(MAKE) -C $(APP) eunit | grep -q "There were no tests to run." + + $i "Generate a module containing EUnit tests in my_app" + $t printf "%s\n" \ + "-module(my_app)." \ + "-ifdef(TEST)." \ + "-include_lib(\"eunit/include/eunit.hrl\")." \ + "ok_test() -> ok." \ + "-endif." > $(APP)/apps/my_app/src/my_app.erl + + $i "Generate a module containing EUnit tests in my_lib" + $t printf "%s\n" \ + "-module(my_lib)." \ + "-ifdef(TEST)." \ + "-include_lib(\"eunit/include/eunit.hrl\")." \ + "ok_test() -> ok." \ + "-endif." > $(APP)/apps/my_lib/src/my_lib.erl + + $i "Check that EUnit runs tests" + $t $(MAKE) -C $(APP) eunit | grep -q "Test passed." + +eunit-check: build clean-eunit-check + + $i "Bootstrap a new OTP application named $(APP)" + $t mkdir $(APP)/ + $t cp ../erlang.mk $(APP)/ + $t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v + + $i "Generate a module containing EUnit tests" + $t printf "%s\n" \ + "-module($(APP))." \ + "-ifdef(TEST)." \ + "-include_lib(\"eunit/include/eunit.hrl\")." \ + "ok_test() -> ok." \ + "-endif." > $(APP)/src/$(APP).erl + + $i "Check that EUnit runs on 'make check'" + $t $(MAKE) -C $(APP) check | grep -q "Test passed." + +eunit-fun: build clean-eunit-fun + + $i "Bootstrap a new OTP application named $(APP)" + $t mkdir $(APP)/ + $t cp ../erlang.mk $(APP)/ + $t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v + + $i "Generate a module containing EUnit tests" + $t printf "%s\n" \ + "-module($(APP))." \ + "-ifdef(TEST)." \ + "-include_lib(\"eunit/include/eunit.hrl\")." \ + "ok_test() -> ok." \ + "bad_test() -> throw(fail)." \ + "-endif." > $(APP)/src/$(APP).erl + + $i "Check that we can run EUnit on a specific test" + $t $(MAKE) -C $(APP) eunit t=$(APP):ok_test $v + +eunit-mod: build clean-eunit-mod + + $i "Bootstrap a new OTP application named $(APP)" + $t mkdir $(APP)/ + $t cp ../erlang.mk $(APP)/ + $t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v + + $i "Generate a module containing EUnit tests" + $t printf "%s\n" \ + "-module($(APP))." \ + "-ifdef(TEST)." \ + "-include_lib(\"eunit/include/eunit.hrl\")." \ + "ok_test() -> ok." \ + "-endif." > $(APP)/src/$(APP).erl + + $i "Generate a module containing failing EUnit tests" + $t printf "%s\n" \ + "-module($(APP)_fail)." \ + "-ifdef(TEST)." \ + "-include_lib(\"eunit/include/eunit.hrl\")." \ + "bad_test() -> throw(fail)." \ + "-endif." > $(APP)/src/$(APP)_fail.erl + + $i "Check that we can run EUnit on a specific module" + $t $(MAKE) -C $(APP) eunit t=$(APP) $v + +eunit-test-dir: build clean-eunit-test-dir + + $i "Bootstrap a new OTP application named $(APP)" + $t mkdir $(APP)/ + $t cp ../erlang.mk $(APP)/ + $t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v + + $i "Generate a module containing EUnit tests" + $t printf "%s\n" \ + "-module($(APP))." \ + "-ifdef(TEST)." \ + "-include_lib(\"eunit/include/eunit.hrl\")." \ + "log_test() -> os:cmd(\"echo $(APP) >> eunit.log\")." \ + "-endif." > $(APP)/src/$(APP).erl + + $i "Generate a module containing EUnit tests in TEST_DIR" + $t mkdir $(APP)/test + $t printf "%s\n" \ + "-module($(APP)_tests)." \ + "-include_lib(\"eunit/include/eunit.hrl\")." \ + "log_test() -> os:cmd(\"echo $(APP)_tests >> eunit.log\")." > $(APP)/test/$(APP)_tests.erl + + $i "Check that EUnit runs both tests" + $t $(MAKE) -C $(APP) eunit | grep -q "2 tests passed." + + $i "Check that tests were both run only once" + $t printf "%s\n" $(APP) $(APP)_tests | cmp $(APP)/eunit.log - + +eunit-tests: build clean-eunit-tests + + $i "Bootstrap a new OTP application named $(APP)" + $t mkdir $(APP)/ + $t cp ../erlang.mk $(APP)/ + $t $(MAKE) -C $(APP) -f erlang.mk bootstrap $v + + $i "Generate a module containing EUnit tests" + $t printf "%s\n" \ + "-module($(APP))." \ + "-ifdef(TEST)." \ + "-include_lib(\"eunit/include/eunit.hrl\")." \ + "ok_test() -> ok." \ + "-endif." > $(APP)/src/$(APP).erl + + $i "Check that EUnit runs on 'make tests'" + $t $(MAKE) -C $(APP) tests | grep -q "Test passed." -- cgit v1.2.3