-
Notifications
You must be signed in to change notification settings - Fork 60
ceylon.test: support parametrized tests #159
Comments
I think we derailed a bit in that discussion :-) here’s a summary: @thradec proposed the following syntax here: {File*} testFiles() => testFileDirectory.files("*.ceylon");
test
void shouldFormatCode(dataProvider(`testFiles`) File f) {
...
} This requires ceylon/ceylon-spec#847. |
Data-driven testing capability is critical. Spock enables this via the where: clause, and usually an @unroll AST transform. TestNG makes use of "data providers". JUnit has the (awkward in my view) Parameterized annotation. Spock (via standard Groovy code) and TestNG (via a special method) allow cross-products over data sets which turns out to be extremely useful. |
Could you explain a bit more? I'm curious about what this is exactly and how it works, but the above could just as well been written in Chinese for me :) |
FTR this issue is blocked by current annotations limitation, see ceylon/ceylon-spec#847 |
@quintesse The (not entirely) trivial example is testing 4 different algorithms for calculating factorial. There are an infinite number of tests for full testing, so you have to make a selection from the domain. Say -100, -10, -1, 0, 1, 4, 10, 100, 1000. Rather than write 36 methods manually, I write 2 methods and some tabular data, and some meta-object protocol (i.e. compiler or run time infrastructure) rewrites my code to generate the 36 methods at run time. This is really trivial with Spock/Groovy, and Python/Pytest, doable with TestNG, and I have never tried with JUnit as I have given up on using it. |
Ok, but I still don't see the advantage, couldn't what you describe be done with a for loop over the data-set and 4 calls to the 4 implementations? |
Although I guess that what you want is that they really show up as different tests, a bit like we do for the language tests for Ceylon in JUnit where we generate our own list of tests on the fly. |
@quintesse looping with four asserts is what has to be done in languages that do not do things correctly! The issue here is one test case means one test method. This way all the tests run all the time, each fail is independent. With loops, you fail on first assert fail and the other tests are not run. |
Well @quintesse, to reiterate what @russel said, with parametrized tests you write a test method as usual, except that it has a nonempty parameter list. Test runner will take that method, create a bunch of test cases based on the data provider for that parameter (see the sample code by lucas above) and run them each one by one as a separate test cases. The advantage of this over for loop is that if your test fails with one use case, you get test failure only for that case and all other cases will pass. |
Thanks @luolong , I now actually understand that example ;) |
An API would be trivial to support: test
shared void normalTest(){}
test
shared void customTest(TestApi api){
api.testParameterised(myTest, 2);
api.testParameterised(myTest, 3);
}
void myTest(Integer param){
//...
} Where |
@FroMage exactly, I probably lean more towards doing thing explicitly myself ;) |
@FroMage That sort of code will just turn people off. The TestNG, Spock and PyTest ways of doing things would be far more suitable. The issue here is that the data must be provided as some table structure not as code. I guess I should publish my Factorial implementations to avoid replicating things here. |
I'm not saying this is a better way, but a very possible workaround that can be implemented now until we support what @thradec needs to implement more powerful annotations. |
FTR, I successfully converted a parametrized TestNG test to a parametrized JUnit test: I find the TestNG way, passing parameters to test, nicer, although experience shows that Eclipse doesn't like to jump to such test... But defining fields in the test class isn't so bad, perhaps. Just to say it can be an alternative for Ceylon... JUnit shows the runs of the test like:
Ie. indeed it highlights only the test that failed for a given parameter. (v = OK; x = failed) |
I hacked together an implementation of this (outside of What would be the advantages of doing this with models instead of declarations? If ceylon/ceylon-spec#847 / eclipse-archived/ceylon#3953 is going to be delayed for much longer, I think it might be worth adding a declaration-based version of this feature soon, and then potentially switch to a model-based version later, when it’s possible. |
now I'm waiting for proper PR 😉
just a little more type safety, we would be able to restrict, that data provider doesn't take any parameters and returns sequence/iterable (and little less typing)
agree, I expected that annotations improvements will be targeted shortly after its first version, but it doesn't seem it would be done soon |
I knew that was coming! :D I might have some time for it this weekend, hopefully.
Actually, I just realized that a data provider could take parameters… as long as they themselves have data providers. I suspect there are actually useful applications of this. |
So I thought about this some more, and realized there’s at least one more way to fill in test function parameters, in addition to data providers: generate random objects. You could consider this a kind of fuzz testing, I think. Given this, I see two possible ways to abstract over this (new interface in addition to
I’m not sure which one is better – I feel like either is more flexible than the other in some aspect. 2 lets you combine different “argument providers” for different arguments of the same function (e. g. one parameter with |
Without deeper thinking, I'm not sure if to go with 1. or 2. or if there is some other way. But you are right, that we have to consider another sources of parameter values (data providers, random values, values injected from DI containers, mocks, ...). Another aspect is, if the test plan (tree of tests) should be static, or if we want to allow dynamically adding tests. |
I decided to go with 1 for now; I think 2 can be emulated with 1, which isn’t true vice versa. The abstraction happens here: TestExecutor createExecutor(FunctionDeclaration funcDecl, ClassDeclaration? classDecl) {
value argumentProvider = DefaultTestArgumentProvider(); // TODO add mechanism to choose argument provider
value executorAnnotation = findAnnotation<TestExecutorAnnotation>(funcDecl, classDecl);
value executorClass = executorAnnotation?.executor else `class DefaultTestExecutor`;
value executors = [
for (arguments in argumentProvider.argumentLists(funcDecl, classDecl))
if (is TestExecutor executor = executorClass.instantiate([], funcDecl, classDecl, arguments))
executor
];
if (nonempty executors) {
if (executors.size == 1) {
return executors.first;
} else {
return GroupTestExecutor(TestDescription(funcDecl.qualifiedName, funcDecl, null, null, executors*.description), executors.sequence());
}
} else {
return ErrorTestExecutor(TestDescription(funcDecl.qualifiedName, funcDecl), Exception("test argument provider ``argumentProvider`` did not provide any argument lists for test function ``funcDecl.qualifiedName``"));
}
} That is, |
First part of eclipse-archived#159. Test function parameters can be annotated with dataProvider (`function provider`), and the provider function or value will be queried for arguments to the test function.
So I have something that works now. No custom However, there’s a large number of test failures in the I’ve pushed what I have so far on my |
What verification? You put the arguments in |
eg. check that all parameters have assigned arg.provider, check that data provider has right signature, check that it returns at least one value, etc if users will see some generic metamodel exception, they will think, that there is a bug in ceylon.test, I think its better to help users discover their faults |
But I don’t agree with those verifications. I think there should also be the possibility to write ArgumentsProviders That’s why I think that your current ArgumentResolver and Other ArgumentsProvider implementations could then act completely An example for an ArgumentsProvider that provides an argument list as a On Tue, Dec 08, 2015 at 01:58:37AM -0800, Tomas Hradec wrote:
|
Well, I can see sometimes differences in naming, what we use, but it seems to me, that the goals are some. So I would recommend to wait on more polished version, which will be soon hopefully ;-), and we can discuss details there. |
Polished version pushed into 159-parameterized-tests branch @ad8f989, let me know if there is missing any interesting use case, or whatever ;-) |
Introduces two new concepts: 1. ArgumentProvider. Provides a stream of arguments for any single parameter. Usually satisfied by an annotation class, where paramaters are annotated with an annotation constructor for that class. Default annotation: argumentProvider, pointing to a stream value or function. 2. ArgumentListProvider. Provides a stream of argument lists for any test function. Default implementation combines arguments from ArgumentProvider annotations on the parameters, but implementations may also obtain arguments in different ways. Specified with the argumentListProvider annotation. See eclipse-archived#159.
Sorry, but I dislike parts of it so much that it motivated me to get off my lazy ass and implement my own vision :D see 0f9c315. With this, I can run the following parameterized tests: https://gist.github.com/lucaswerkmeister/1be334f4ca140ac051d2 Important to me:
Not important to me:
Open problems with my version:
What I mainly dislike about your version is that your |
No problem, this is important feature, so it should be think through all sides ;-). In fact, in the beginning I had two independent interfaces (ArgumentProvider/ArgumentListProvider) as you too. Then I realised, that return type of ArgumentProvider is not always {Anything*}, because for DI or mocking there is just one returned value. So I changed it to Anything, and because it can return whatever, it can takes the role of ArgumentListProvider also. The only disadvantage are 2-3 if statements in ArgumentResolver. And, from user perspective, it doesn't bring any advantage, to keep it separately. He wants simple way, how to express, where argument values are provided. We are not able to warrant type safety anyway. (just the reasoning behind) I think, I don't mind to split it again, indeed its more clean (from ArgumentResolver PoV), and I can keep simplicity for user by satisfying both interfaces (for cases where it make sense). |
it's crucial to postpone arguments resolving as late as possible (there is always some @jvasileff ;-) who wants to run 100k tests) |
IDE support added in eclipse-archived/ceylon-ide-eclipse@4073017 and c06672e. |
Rebased and merged to master, via facc616 and eclipse-archived/ceylon-ide-eclipse@2c68a85. Recapitulation:
Example:
I'm going to close this issue, for any improvements or new usecases, please open new issue. |
👍 Good work! |
- Many, MANY tests were calling assertEquals with the wrong argument order: assertEquals(expected, actual), whereas the correct order with positional arguments is assertEquals(actual, expected). A few of these mistakes were caught by #6183. I attempted to fix these in a way that would be consistent with other tests in the file, so sometimes I swapped the parameters, and sometimes I changed the invocation to use named arguments. - Some tests did manual exception handling instead of using assertThatException. - Some tests could be parameterized (eclipse-archived#159).
- Many, MANY tests were calling assertEquals with the wrong argument order: assertEquals(expected, actual), whereas the correct order with positional arguments is assertEquals(actual, expected). A few of these mistakes were caught by eclipse-archived/ceylon#6183. I attempted to fix them in a way that would be consistent with other tests in the file, so sometimes I swapped the parameters, and sometimes I changed the invocation to use named arguments. - Some tests did manual exception handling instead of using assertThatException. - Some tests could be parameterized (eclipse-archived#159).
- Many, MANY tests were calling assertEquals with the wrong argument order: assertEquals(expected, actual), whereas the correct order with positional arguments is assertEquals(actual, expected). A few of these mistakes were caught by eclipse-archived/ceylon#6183. I attempted to fix them in a way that would be consistent with other tests in the file, so sometimes I swapped the parameters, and sometimes I changed the invocation to use named arguments. - Some tests did manual exception handling instead of using assertThatException. - Some tests could be parameterized (eclipse-archived#159). - Some date_* variables were misspelled as data_*.
see discussion in #158
The text was updated successfully, but these errors were encountered: