Join GitHub today
GitHub is home to over 40 million developers working together to host and review code, manage projects, and build software together.Sign up
Route pedestrians over highway=platform, define it as a pushing section for bikes. Unit testing considerations. #1614
Correct foot routing:
Correct bike routing where gh prefers taking the road over entering the pushing section:
Correct bike routing where an intermediate node forces gh to enter the pushing section:
The fix was relatively straightforward, not sure what much to say about it, just adding the correct definitions for highway=platform in foot and bike encoder classes.
Reading the documentation of Tag:highway=platform did illustrate that
I took the time to look at the test unit structure and think about improvements, since @karussell seemed interested in what I could do on the unit testing side, so I ended up writing the unit tests for fix in a different pattern.
I realized this should probably be in a different pull request altogether only after being three parts done. That's mea culpa. I wouldn't mind rewriting the tests in the current pattern for this PR and opening a separate one if that's needed, or just using the current pattern if the change is unwelcome. Just to be clear, I haven't touched any existing test logic or structure at all, just wrote new tests.
Unit Testing pattern
I noticed the inheritance hierarchy in the test structure and the lack of parameterized tests and I think I get why. JUnit doesn't really let you parameterize on a per method basis, just on a per class basis and that can be really clumsy. Having to deal with that and the need to share code logic from abstract to concrete can make things pretty hairy. However, one can make use of JUnit's TestRule and Parameterized to tackle both issues at once.
The two test classes I added are:
BikeFlagEncoderRule sort of does what a before or setup annotation would do, except it encapsulates the same data that
@ClassRule public static BikeFlagEncoderRule encoderRule = new BikeFlagEncoderRule(new BikeFlagEncoder());
Now the entire test class can use any data in encoderRule and encoderRule can hold all the data AbstractBikeFlagEncoder used to hold. Note that the usage of abstract is no longer needed because the constructor to the concrete implementation is called in the constructor of BikeFlagEncoderRule.
This opens the way to replacing the inheritance pattern with a composition pattern. That would allow the test logic in AbstractBikeFlagEncoder to moved into something more akin to a helper class, so the tests can be shared without needing inheritance.
Among other things, this has the significant benefit of making the usage of Parameterized easier. To deal with the difficulty of mixing disparate methods under a regime that requires that test parameterization happen class-wide, I propose using org.junit.experimental.runners.Enclosed.
Enclosed allows static nested classes in a test class to be run properly. Why do we want static nested classes? They allow us to constrain Parameterized and have it be scoped only with the methods that need that parameter list. This allows unparameterized tests to just exist at the top level and parameterized tests on the second level, grouped by their parameter list.
Right now, the way
tl;dr if this sounds interesting, I could make a new PR and rewrite:
to use a parameterized test pattern without inheritance.
karussell left a comment •
This looks good - thanks again!
Thanks also for the insights in the approach regarding the test rules. The reason we use this inheritance is very likely that we can easily customize on a per-method base (like you said).
Can you explain why we need the two new test classes BikeFlagEncoderRule.java and BikeFlagEncoderDismountTest.java ? I think the handling for the different surfaces should be already covered from other tests. I would prefer this new approach separate in a different issue. Maybe where it is compared to the existing code and I better understand which advantages we would get.
Understood, that's the best way to deal with it. I'll open a different PR for the test pattern proposal. I'll stash away the new tests and just write the tests for this bug fix in the current style for this PR, so it's kept separate. Probably this weekend.
I thought again about this and the inheritance itself is mostly fine. I thought about how it could be replaced but it's not immediately obvious to me that the tradeoffs are worth it. Without inheritance it becomes annoying to share unit test logic from base/abstract to child/concrete classes. Using rules there might be a way to get JUnit to run the base tests on a child/concrete instance. And using the rule I defined definitely lets you get away without using virtual / override, which reduces coupling a bit. But I also found out that Rules are apparently on legacy support in JUnit5, which is something to consider. I'll take a deep dive into the API again to figure things out. This part is significantly less important than the parameterization anyway.
BikeFlagEncoderRule.java appeared because defining a rule requires its own class. The rule makes available to the actual tests everything that they would have available if they had inherited from
BikeFlagEncoderDismountTest.java appeared because I used the Parameterized annotation. JUnit4 seems to be able to only parameterize on a class-wide basis and not on a per method basis. It felt very wrong to edit an existing test file and make it run with parameterize, so I made a new class.
In unit tests, I try to test something as close to a single atomic behavior as possible. In this case the behavior being tested is in what conditions does GH decide its necessary to get off the bike. Since the relevant conditions are essentially highway value, surface and whether the node is part of a cycle route or not, you can neatly separate test logic and data.
Why do any of this? Well, there's a few reasons:
The way the tests are written right now, you're essentially running the same method several times with different parameters in the same test method. The tests follow a setup - assert or setup - assert - reset pattern. The latter one can be especially prone to bugs, if you forget to reset or if previous lines mutated something. The unit tests lose their isolation. And when the test method fails you need to check at which point it failed. Using test parameterization, each instance of the test is effectively isolated, so you can't make a mistake and the test explorer readily shows you on which parameters it failed and on which it didn't, which makes diagnosing why a test failed much faster. Parameterized tests also let you add or remove cases without touching any of the test logic at all. As it is, the test data and logic are essentially mixed.
Moving to JUnit5 would massively simply things because of the existence of @ParameterizedTest which lets you parameterize each method in part. I'm not sure what your opinion is on a JUnit5 migration, but if you're unsure or interested about it, I can look into what other advantages JUnit5 would bring and how difficult the migration would be and present that later.
At this point I'm getting a bit self conscious about looking like I'm wasting people's time with gold plating unit tests when there's possibly much more pressing features to deal with. But I think that having parameterized tests is important.
For me better support for parameterized testing, e.g. on a per-method basis, would have been useful a couple of times so far. I feel solving this via inheritance sometimes makes the tests harder to understand. I would definitely be interested how JUnit5 can improve the situation and how much effort such a migration would take. Then again so far I did not run into any situation where the current Junit4 setup was a big hindrance and I really felt the need for something else.
I discarded the commits that were related to the proposed test unit pattern and wrote the rest of the required tests. This should be good to go now and I'll be writing a PR for the unit test pattern sometime next week.
I'm not sure where to discuss my findings about JUnit5 migration? The unit test proposal PR? Open a separate issue? Some other channel?