Testing locally
Questions
How hard is it to set up a test suite for a first unit test?
Exercise
In this exercise we will make a simple function and use one of the language specific test frameworks to test it.
This is easy to use by almost any project and doesn’t rely on any other servers or services.
The downside is that you have to remember to run it yourself.
Local-1: Create a minimal example (15 min)
In this exercise, we will create a minimal example using the pytest, run the test, and show what happens when a test breaks.
Create a new directory and change into it:
$ mkdir local-testing-example $ cd local-testing-example
Create an example file and paste the following code into it
Create example.py
with content
def add(a, b):
return a + b
def test_add():
assert add(2, 3) == 5
assert add('space', 'ship') == 'spaceship'
This code contains one genuine function and a test function.
pytest
finds any functions beginning with test_
and treats them
as tests.
Create example.R
with content
if (!require(testthat)) install.packages(testthat)
add <- function(a, b) {
return(a + b)
}
test_that("Adding integers works", {
res <- add(2, 3)
# Test that the result has the correct value
expect_identical(res, 5)
# Test that the result is numeric
expect_true(is.numeric(res))
})
A test with testthat
is created by calling
test_that()
with a test name and code as arguments.
Create example.jl
with content
function myadd(a,b)
return a+b
end
using Test
@testset "myadd" begin
@test myadd(2,3) == 5
end
The package Test.jl
handles all testing.
A test(set) is added with @testset
and a test itself with @test
.
Create example.cc
with content
#include <catch2/catch_test_macros.hpp>
template<typename Number>
Number
add(Number a, Number b)
{
return a + b;
}
TEST_CASE("IntTest", "[add]")
{
REQUIRE(add(2,3)==5);
}
and CMakeLists.txt
with content
cmake_minimum_required(VERSION 3.14)
project(test_example CXX)
cmake_policy(SET CMP0135 OLD)
#Tell CMake to Download Catch2 library
include(FetchContent)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.4.0 # or a later release
)
FetchContent_MakeAvailable(Catch2)
#Setup testing
enable_testing()
#add test executable
add_executable(example example.cc)
#link Catch2 to example
target_link_libraries(example PRIVATE Catch2::Catch2WithMain)
#Make Catch2 look for tests
include(Catch)
catch_discover_tests(example)
Create example.cc
with content
#include <gtest/gtest.h>
template<typename Number>
Number
add(Number a, Number b)
{
return a + b;
}
TEST(AddTests, IntTest)
{
ASSERT_EQ(add(2,3),5);
}
and CMakeLists.txt
with content
cmake_minimum_required(VERSION 3.14)
project(test_example CXX)
cmake_policy(SET CMP0135 OLD)
#Tell CMake to Download Googletest library
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
)
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
#Setup testing
enable_testing()
#add test executable
add_executable(example example.cc)
#link Googletest to example
target_link_libraries(example GTest::gtest_main)
#Make googletest look for tests
include(GoogleTest)
gtest_discover_tests(example)
Run the test
$ pytest -v example.py
============================================================ test session starts =================================
platform linux -- Python 3.7.2, pytest-4.3.1, py-1.8.0, pluggy-0.9.0 -- /home/user/pytest-example/venv/bin/python3
cachedir: .pytest_cache
rootdir: /home/user/pytest-example, inifile:
collected 1 item
example.py::test_add PASSED
========================================================= 1 passed in 0.01 seconds ===============================
Yay! The test passed!
Hint for participants trying this inside Spyder or IPython: try !pytest -v example.py
.
$ Rscript example.R
Loading required package: testthat
Test passed 🎉
Yay! The test passed!
Note that the emoji is random and might be different for you.
$ julia example.jl
Test Summary: | Pass Total Time
myadd | 1 1 0.0s
Yay! The test passed!
As this is the only compiled language in the examples we have to do a few more steps.
Starting with the configure step of CMake
(Note that <path>
will be different for everyone)
$ cmake -S. -Bbuild
-- The CXX compiler identification is GNU 13.2.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Performing Test HAVE_FLAG__ffile_prefix_map__<path>_build__deps_catch2_src__
-- Performing Test HAVE_FLAG__ffile_prefix_map__<path>_build__deps_catch2_src__ - Success
-- Configuring done (4.1s)
-- Generating done (0.0s)
-- Build files have been written to: <path>/build
and the build step which will also download Catch2 on the first run
$ cmake --build build
[ 0%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/benchmark/catch_chronometer.cpp.o
[ 1%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/benchmark/detail/catch_benchmark_function.cpp.o
[ 2%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/benchmark/detail/catch_run_for_at_least.cpp.o
[ 3%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/benchmark/detail/catch_stats.cpp.o
[ 4%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/generators/catch_generator_exception.cpp.o
[ 5%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/generators/catch_generators.cpp.o
[ 6%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/generators/catch_generators_random.cpp.o
[ 7%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/reporters/catch_reporter_automake.cpp.o
[ 8%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/reporters/catch_reporter_common_base.cpp.o
[ 9%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/reporters/catch_reporter_compact.cpp.o
[ 10%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/reporters/catch_reporter_console.cpp.o
[ 11%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/reporters/catch_reporter_cumulative_base.cpp.o
[ 12%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/reporters/catch_reporter_event_listener.cpp.o
[ 12%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/reporters/catch_reporter_helpers.cpp.o
[ 13%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/reporters/catch_reporter_junit.cpp.o
[ 14%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/reporters/catch_reporter_multi.cpp.o
[ 15%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/reporters/catch_reporter_registrars.cpp.o
[ 16%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/reporters/catch_reporter_sonarqube.cpp.o
[ 17%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/reporters/catch_reporter_streaming_base.cpp.o
[ 18%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/reporters/catch_reporter_tap.cpp.o
[ 19%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/reporters/catch_reporter_teamcity.cpp.o
[ 20%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/reporters/catch_reporter_xml.cpp.o
[ 21%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/interfaces/catch_interfaces_capture.cpp.o
[ 22%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/interfaces/catch_interfaces_config.cpp.o
[ 23%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/interfaces/catch_interfaces_exception.cpp.o
[ 24%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/interfaces/catch_interfaces_generatortracker.cpp.o
[ 25%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/interfaces/catch_interfaces_registry_hub.cpp.o
[ 25%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/interfaces/catch_interfaces_reporter.cpp.o
[ 26%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/interfaces/catch_interfaces_reporter_factory.cpp.o
[ 27%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/interfaces/catch_interfaces_testcase.cpp.o
[ 28%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/catch_approx.cpp.o
[ 29%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/catch_assertion_result.cpp.o
[ 30%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/catch_config.cpp.o
[ 31%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/catch_get_random_seed.cpp.o
[ 32%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/catch_message.cpp.o
[ 33%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/catch_registry_hub.cpp.o
[ 34%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/catch_session.cpp.o
[ 35%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/catch_tag_alias_autoregistrar.cpp.o
[ 36%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/catch_test_case_info.cpp.o
[ 37%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/catch_test_spec.cpp.o
[ 37%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/catch_timer.cpp.o
[ 38%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/catch_tostring.cpp.o
[ 39%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/catch_totals.cpp.o
[ 40%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/catch_translate_exception.cpp.o
[ 41%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/catch_version.cpp.o
[ 42%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_assertion_handler.cpp.o
[ 43%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_case_insensitive_comparisons.cpp.o
[ 44%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_clara.cpp.o
[ 45%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_commandline.cpp.o
[ 46%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_console_colour.cpp.o
[ 47%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_context.cpp.o
[ 48%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_debug_console.cpp.o
[ 49%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_debugger.cpp.o
[ 50%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_decomposer.cpp.o
[ 50%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_enforce.cpp.o
[ 51%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_enum_values_registry.cpp.o
[ 52%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_errno_guard.cpp.o
[ 53%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_exception_translator_registry.cpp.o
[ 54%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_fatal_condition_handler.cpp.o
[ 55%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_floating_point_helpers.cpp.o
[ 56%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_getenv.cpp.o
[ 57%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_istream.cpp.o
[ 58%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_lazy_expr.cpp.o
[ 59%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_leak_detector.cpp.o
[ 60%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_list.cpp.o
[ 61%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_message_info.cpp.o
[ 62%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_output_redirect.cpp.o
[ 62%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_parse_numbers.cpp.o
[ 63%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_polyfills.cpp.o
[ 64%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_random_number_generator.cpp.o
[ 65%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_random_seed_generation.cpp.o
[ 66%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_reporter_registry.cpp.o
[ 67%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_reporter_spec_parser.cpp.o
[ 68%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_result_type.cpp.o
[ 69%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_reusable_string_stream.cpp.o
[ 70%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_run_context.cpp.o
[ 71%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_section.cpp.o
[ 72%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_singletons.cpp.o
[ 73%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_source_line_info.cpp.o
[ 74%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_startup_exception_registry.cpp.o
[ 75%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_stdstreams.cpp.o
[ 75%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_string_manip.cpp.o
[ 76%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_stringref.cpp.o
[ 77%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_tag_alias_registry.cpp.o
[ 78%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_test_case_info_hasher.cpp.o
[ 79%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_test_case_registry_impl.cpp.o
[ 80%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_test_case_tracker.cpp.o
[ 81%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_test_failure_exception.cpp.o
[ 82%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_test_registry.cpp.o
[ 83%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_test_spec_parser.cpp.o
[ 84%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_textflow.cpp.o
[ 85%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_uncaught_exceptions.cpp.o
[ 86%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_wildcard_pattern.cpp.o
[ 87%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/internal/catch_xmlwriter.cpp.o
[ 87%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/matchers/catch_matchers.cpp.o
[ 88%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/matchers/catch_matchers_container_properties.cpp.o
[ 89%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/matchers/catch_matchers_exception.cpp.o
[ 90%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/matchers/catch_matchers_floating_point.cpp.o
[ 91%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/matchers/catch_matchers_predicate.cpp.o
[ 92%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/matchers/catch_matchers_quantifiers.cpp.o
[ 93%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/matchers/catch_matchers_string.cpp.o
[ 94%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/matchers/catch_matchers_templated.cpp.o
[ 95%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2.dir/catch2/matchers/internal/catch_matchers_impl.cpp.o
[ 96%] Linking CXX static library libCatch2.a
[ 96%] Built target Catch2
[ 97%] Building CXX object _deps/catch2-build/src/CMakeFiles/Catch2WithMain.dir/catch2/internal/catch_main.cpp.o
[ 98%] Linking CXX static library libCatch2Main.a
[ 98%] Built target Catch2WithMain
[ 99%] Building CXX object CMakeFiles/example.dir/example.cc.o
[100%] Linking CXX executable example
[100%] Built target example
Finally, we change into the build
directory
and run CTest
$ cd build
$ ctest
Test project <path>/build
Start 1: IntTest
1/1 Test #1: IntTest .......................... Passed 0.01 sec
100% tests passed, 0 tests failed out of 1
Total Test time (real) = 0.01 sec
Starting with the configure step of CMake
(Note that <path>
will be different for everyone)
$ cmake -S. -Bbuild
-- The CXX compiler identification is GNU 13.2.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- The C compiler identification is GNU 13.2.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD - Success
-- Found Threads: TRUE
-- Configuring done (2.1s)
-- Generating done (0.0s)
-- Build files have been written to: <path>/build
and the build step which will also download GoogleTest on the first run
$ cmake --build build
[ 10%] Building CXX object _deps/googletest-build/googletest/CMakeFiles/gtest.dir/src/gtest-all.cc.o
[ 20%] Linking CXX static library ../../../lib/libgtest.a
[ 20%] Built target gtest
[ 30%] Building CXX object _deps/googletest-build/googletest/CMakeFiles/gtest_main.dir/src/gtest_main.cc.o
[ 40%] Linking CXX static library ../../../lib/libgtest_main.a
[ 40%] Built target gtest_main
[ 50%] Building CXX object CMakeFiles/example.dir/example.cc.o
[ 60%] Linking CXX executable example
[ 60%] Built target example
[ 70%] Building CXX object _deps/googletest-build/googlemock/CMakeFiles/gmock.dir/src/gmock-all.cc.o
[ 80%] Linking CXX static library ../../../lib/libgmock.a
[ 80%] Built target gmock
[ 90%] Building CXX object _deps/googletest-build/googlemock/CMakeFiles/gmock_main.dir/src/gmock_main.cc.o
[100%] Linking CXX static library ../../../lib/libgmock_main.a
[100%] Built target gmock_main
Finally, we change into the build
directory
and run CTest
$ cd build
$ ctest
Test project <path>/build
Start 1: AddTests.IntTest
1/1 Test #1: AddTests.IntTest ................. Passed 0.00 sec
100% tests passed, 0 tests failed out of 1
Total Test time (real) = 0.01 sec
Let us break the test!
Introduce a code change which breaks the code (e.g. -
instead of +
) and check
whether our test detects the change:
$ pytest -v example.py
============================================================ test session starts =================================
platform linux -- Python 3.7.2, pytest-4.3.1, py-1.8.0, pluggy-0.9.0 -- /home/user/pytest-example/venv/bin/python3
cachedir: .pytest_cache
rootdir: /home/user/pytest-example, inifile:
collected 1 item
example.py::test_add FAILED
================================================================= FAILURES =======================================
_________________________________________________________________ test_add _______________________________________
def test_add():
> assert add(2, 3) == 5
E assert -1 == 5
E --1
E +5
example.py:6: AssertionError
========================================================= 1 failed in 0.05 seconds ==============
Notice how pytest is smart and includes context: lines that failed, values of the relevant variables.
$ Rscript example.R
── Failure: Adding integers works ──────────────────────────────
`res` not identical to 5.
1/1 mismatches
[1] -1 - 5 == -6
Error: Test failed
Execution halted
testthat
tells us exactly which test failed and how
but does not include more context.
$ julia example.jl
myadd: Test Failed at /home/user/local-testing-example/example.jl:7
Expression: myadd(2, 3) == 5
Evaluated: -1 == 5
Stacktrace:
[1] macro expansion
@ ~/opt/julia-1.10.0/share/julia/stdlib/v1.10/Test/src/Test.jl:672 [inlined]
[2] macro expansion
@ ~/local-testing-example/example.jl:7 [inlined]
[3] macro expansion
@ ~/opt/julia-1.10.0/share/julia/stdlib/v1.10/Test/src/Test.jl:1577 [inlined]
[4] top-level scope
@ ~/local-testing-example/example.jl:7
Test Summary: | Fail Total Time
myadd | 1 1 0.6s
ERROR: LoadError: Some tests did not pass: 0 passed, 1 failed, 0 errored, 0 broken.
in expression starting at /home/user/local-testing-example/example.jl:6
Notice how Test.jl
is smart and includes context:
Lines that failed, evaluated and expected results.
Since we introduced a change we have to call cmake --build build
again
(from the top directory)
$ ctest
Test project <path>/build
Start 1: IntTest
1/1 Test #1: IntTest ..........................***Failed 0.01 sec
0% tests passed, 1 tests failed out of 1
Total Test time (real) = 0.01 sec
The following tests FAILED:
1 - IntTest (Failed)
Output from these tests are in: <path>/build/Testing/Temporary/LastTest.log
Use "--rerun-failed --output-on-failure" to re-run the failed cases verbosely.
following the recommendation, we run
$ ctest --output-on-failure
Test project <path>/build
Start 1: IntTest
1/1 Test #1: IntTest ..........................***Failed 0.01 sec
Filters: "IntTest"
Randomness seeded to: 2865819826
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
example is a Catch2 v3.4.0 host application.
Run with -? for options
-------------------------------------------------------------------------------
IntTest
-------------------------------------------------------------------------------
<path>/example.cc:10
...............................................................................
<path>/example.cc:12: FAILED:
REQUIRE( add(2,3)==5 )
with expansion:
-1 == 5
===============================================================================
test cases: 1 | 1 failed
assertions: 1 | 1 failed
0% tests passed, 1 tests failed out of 1
Total Test time (real) = 0.01 sec
The following tests FAILED:
1 - IntTest (Failed)
Errors while running CTest
This output is very verbose, but notice how it includes context: lines that failed, values of the relevant variables
$ ctest
Test project <path>/build
Start 1: AddTests.IntTest
1/1 Test #1: AddTests.IntTest .................***Failed 0.00 sec
0% tests passed, 1 tests failed out of 1
Total Test time (real) = 0.01 sec
The following tests FAILED:
1 - AddTests.IntTest (Failed)
Errors while running CTest
Output from these tests are in: <path>/build/Testing/Temporary/LastTest.log
Use "--rerun-failed --output-on-failure" to re-run the failed cases verbosely.
following the recommendation, we run
$ ctest --output-on-failure
Test project <path>/build
Start 1: AddTests.IntTest
1/1 Test #1: AddTests.IntTest .................***Failed 0.00 sec
Running main() from <path>/build/_deps/googletest-src/googletest/src/gtest_main.cc
Note: Google Test filter = AddTests.IntTest
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from AddTests
[ RUN ] AddTests.IntTest
<path>/example.cc:12: Failure
Expected equality of these values:
add(2,3)
Which is: -1
5
[ FAILED ] AddTests.IntTest (0 ms)
[----------] 1 test from AddTests (0 ms total)
[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[ PASSED ] 0 tests.
[ FAILED ] 1 test, listed below:
[ FAILED ] AddTests.IntTest
1 FAILED TEST
0% tests passed, 1 tests failed out of 1
Total Test time (real) = 0.01 sec
The following tests FAILED:
1 - AddTests.IntTest (Failed)
Errors while running CTest
This output is very verbose, but notice how it includes context: lines that failed, values of the relevant variables
(optional) Local-2: Create a test that considers numerical tolerance (10 min)
Let’s see an example where the test has to be more clever in order to avoid false negative.
In the above exercise we have compared integers. In this optional exercise we want to learn how to compare floating point numbers since they are more tricky (see also “What Every Programmer Should Know About Floating-Point Arithmetic”).
The following test will fail and this might be surprising. Try it out:
def add(a, b):
return a + b
def test_add():
assert add(0.1, 0.2) == 0.3
add <- function(a, b){
return a + b
}
test_that("Adding floats works", {
expect_identical(add(0.1, 0.2),0.3)
})
function myadd(a,b)
return a + b
end
using Test
@testset "myadd" begin
@test myadd(0.1, 0.2) == 0.3
end
#include <catch2/catch_test_macros.hpp>
template<typename Number>
Number
add(Number a, Number b)
{
return a + b;
}
TEST_CASE("AddTests", "DoubleTest")
{
REQUIRE(add(0.1,0.2)==0.3);
}
#include <gtest/gtest.h>
template<typename Number>
Number
add(Number a, Number b)
{
return a + b;
}
TEST(AddTests, DoubleTest)
{
ASSERT_EQ(add(0.1,0.2),0.3);
}
Your goal: find a more robust way to test this addition.
Solution: Local-2
One solution is to use pytest.approx:
from pytest import approx
def add(a, b):
return a + b
def test_add():
assert add(0.1, 0.2) == approx(0.3)
But maybe you didn’t know about pytest.approx: and did this instead:
def test_add():
result = add(0.1, 0.2)
assert abs(result - 0.3) < 1.0e-7
This is OK but the 1.0e-7
can be a bit arbitrary.
One solution is to use expect_equal which allows for roundoff errors:
test_that("Adding floats works with equal", {
res <- add(0.1, 0.2)
expect_equal(res,0.3)
expect_true(is.numeric(res))
})
But maybe you didn’t know about it and used the ‘less than’ comparison of expect_lt instead:
test_that("Adding floats works with lt", {
res <- add(0.1, 0.2)
expect_lt(abs(res-0.3),1.0e-7)
expect_true(is.numeric(res))
})
This is OK but the 1.0e-7
can be a bit arbitrary.
One solution is to use \approx
:
@testset "Add floats with approx" begin
@test myadd(0.1,0.2) ≈ 0.3
#Variant with specifying a tolerance
@test myadd(0.1,0.2) ≈ 0.3 atol=1.0e-7
end
But maybe you didn’t know about \approx
and did this instead:
@test abs(myadd(0.1,0.2)-0.3) < 1.0e-7
This is OK but the 1.0e-7
can be a bit arbitrary.
Catch2 offers a very sophisticated way to test equality of two floating point numbers based on ‘units in the last place (ULP)’.
Matching the settings of GoogleTest we consider decimals equals to within 4
ULP.
#include <catch2/matchers/catch_matchers_floating_point.hpp>
using Catch::Matchers::WithinULP;
TEST_CASE("DoubleTest", "[add]")
{
REQUIRE_THAT(add(0.1,0.2),WithinULP(0.3,4));
}
But maybe you didn’t know about Catch2 floating point matchers and did this instead:
#include <cmath>
TEST_CASE("DoubleTest", "[add]")
{
REQUIRE(std::abs(add(0.1,0.2)-0.3) < 1.0e-7);
}
This is OK but the 1.0e-7
can be a bit arbitrary. The first option is certainly more robust and recommended in the C++ case.
GoogleTest offers a very sophisticated way to test equality of two double numbers based on ‘units in the last place (ULP)’, see the documentation for more details.
TEST(AddTests, DoubleTest)
{
ASSERT_DOUBLE_EQ(add(0.1,0.2),0.3);
}
But maybe you found the ‘less than’ assertion first and did this instead:
#include<cmath>
TEST(AddTests, DoubleTest)
{
//assert that add(0.1,0.2)-0.3 < 1.0e-7 holds
ASSERT_LT(std::abs(add(0.1,0.2)-0.3),1.0e-7);
}
This is OK but the 1.0e-7
can be a bit arbitrary. The first option is certainly more robust and recommended in the C++ case.
Keypoints
Each test framework has its way of collecting and running all test functions, e.g. functions beginning with
test_
forpytest
.Python, Julia and C/C++ have better tooling for automated tests than Fortran and you can use those also for Fortran projects (via
iso_c_binding
).