-
-
Notifications
You must be signed in to change notification settings - Fork 208
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improving unit test developer experience #1753
Comments
Let's break this down, first regarding setup / teardown. This is harder than you think. While you can add setup and teardown for ALL tests by using Because a test does not need to have a module marked with This means that there is no strict way to link a test and a setup. So unfortunately that has to be explicit calls. teardown is simple enough to add using |
In addition, I must add a personal note that I've not found setup/teardown that useful when writing tests in various languages. |
Regarding asserts for testing. Those are trivial to extend from existing macros, and people are encouraged to add those. There are several missing asserts, not the least an assert that ensures that something does indeed fail as expected! There was someone working on this, but it never got submitted to the stdlib. Pull Requests with improvements are encouraged! |
Regarding better backtraces and better stops: The testrunner is intended to be pluggable. An IDE can integrate with the tests by passing an alternative test runner function for example. The current test runner is really a minimum viable product, lots of things could be made to improve the default test runner. |
For Single Test Runner I am unsure of what you want to achieve. What do you want to single out? |
Regarding mocks there is some work towards The idea is essentially that if you have fn void foo() @weak { ... } And then add another file with fn void foo() { ... } Then this is not considered a collision, but instead the latter completely replaces the former. Similarly we could imagine: fn void foo2() { ... } And then add another file with fn void foo2() @override { ... } Then the behaviour would be the same. That the latter is replacing the former. Could not this offer a good way to mock things? |
Ok, I understood. Maybe direct call + defer is the way to go, at least it fits Python's principle While experimenting along those lines I've found that test's defer was not called after assert hit. Consider this example: MyTestSuite t @local = {
.setup_fn = fn void() {
io::printn("\nsetup func");
},
.teardown_fn = fn void() {
io::printn("\nteardown func");
},
};
fn void test_sub() {
t.setup();
defer t.teardown(); // teardown func was not called when assert failes
// NOTE: this fails
assert( 1 == 0 );
}
|
At minimum, it would be great to have test output only for a single test module (maybe as a test case filters). However, I think it worth discussing single program per test file build. This will help to isolate some big chunks of larger projects, and apply mocking with So having 1 executable per test may solve these types of conflicts, this will make testing more flexible and hackable. |
No octal escapes are supported in the formatter. In this case |
Remember to check that the terminal accepts ANSI codes before sending them. |
@lerno Can we discuss test macros naming question? I rejected the idea of having TestSuite struct entity, because it feels as mental overhead to me and unfit to single To me, option 1 has more c3 flavor, and it's less to type also :) |
@lerno could you suggest a proper way of adding CLI parameters to the test runners? I was experimenting with default runner, and was able to add some useful features:
There are other ideas floating around, for example quiet test mode (suppressing stdout verbosity of tests cases and possibly user code). Conceptually, test filter may work like this (filter is real, but CLI parameter is fake): |
I altered test report layout to solve the following issues:
@lerno let me know if this looks good to you. thx |
I think I've figured out the fn void test_sub() {
test::setup(
// mandatory setup function it's called immediately
setup_fn: fn void!() {
io::printfn("setup fn");
},
// optional teardown function it's called after test finish (or failed)
teardown_fn: fn void!() {
io::printfn("teardown_fn");
}
);
test::eq(3, foo::sub(3, 1));
|
This is looking great, sorry for not getting back to you in time. I'll look at this tomorrow. |
I added sample This is an example, of how a sample unit test will look like: |
Hi @lerno, thank you for a such great language, and I really appreciate the clarity and simplicity of c3 code. I'd like to discuss direction of unit testing part of the language and how to improve developer productivity using c3 test driven development approach.
I'd like to improve the unit testing experience of the c3, and I wanted to know your vision of the problem I'd like to solve, or if you consider them as problems at all.
List of improvements I think would be useful in unit test experience
(The ideas below are the subject of discussion, let me know what you think)
Setup / teardown test functions
It would be great to have setup/teardown functions for test modules (i.e. marked as
module test_something @test;
). Because in production code, sometimes we need to setup environment (DB, socket, mocks) for each test case in a suite and make some cleanup afterward.Example:
Special test asserts
Using built-in
assert
maybe is not optimal for productive test driven development. For example, failure of this assertassert(6 == bar::mul(2,3));
produces an error:However, it's not clear what value was returned by
bar::mul(2,3))
, to figure it out we need to add formatting to assert, like this:assert(6 == bar::mul(2,3), "failed: %s", bar::mul(2,3));
. In my experience, maintaining of this approach quickly becomes a hell. So my proposal is to introduce special test asserts which include expected/actual value printing.Conceptually, it could be a special macro for this, i.e.
tassert_eq(a, b);
or maybe test suite struct. Concept code which compiles and works below:Having this value printouts on failures, significantly improving development and debugging velocity. And the developer gets ideas of what's going wrong much faster, with this error message:
Code asserts should produce backtrace
Debugging assert failure inside test function might be trivial, but when assert (or
@require
/@ensure
) was triggered somewhere deep in the guts of the project, it's not trivial what caused the failure.Simple example:
test_add fails with this obscure error:
It's not clear when it was triggered, especially if we would have multiple non-obvious calls of an asserted function:
Backtrace printing is the solution for this problem (I made some proof of the concept hacking of c3 test runner):
The side effect of this, however, that all failed asserts will start producing back traces. So that's why we possibly need to introduce special test asserts.
Code asserts should be debuggable (in unit tests)
It would be great we could run debugger and automatically stop at assert trigger in the code. I did it a lot in my
C
projects, butc3
test runner is designed a bit different, which hardens debugability of unit tests. But I think it's a solvable issue.Single test file runner
It's usually crucial in TDD cycle to work with dedicated test file, so it would be great if we could do something like this
c3c test test/test_foo.c3
. I'm aware of c3c optioncompile-test
however, the downside of it that it requires to pass all other sources in order to compile the test.Implementing c3 mocks
Sometimes mocks are crucial in building production applications, and sometimes we need to decouple code (for example mocking hardware related functions in embedded device). It's a very broad and complicated topic, however there are some solutions for C mocking (fff.h or C-mock). Or at least we might try linker wrap options sample gist.
At least for now, the mocking stuff might have a mandatory requirement of separating all tests in stand-alone executables. This is long discussion, we may touch it later down the road.
It is a long list, but I think that everything is doable. I can take burden of implementation. @lerno before I start I'd like to know your view on this problem. Maybe my proposals may interfere with minimalistic nature of c3, that's ok, I'll make my own project. But frankly
I think this should be a feature of the language.
The text was updated successfully, but these errors were encountered: