-
Notifications
You must be signed in to change notification settings - Fork 0
Unit-test refactoring; removed some unused code too. #39
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My opinion on the removal of theories in InputValidationTest
is that it doesn't really improve the tests, and makes the tests themselves a bit more complicated. I do think it is a good idea to consistently use google truth, but we can we google truth in a test run with JUnit theories; all that JUnit theories allows us to do is more easily run the tests with many arguments.
While the removal of JUnit theories does mean we don't need the additional code in the theories
directory to support it, I don't think that is a sufficient reason to remove them.
@Test | ||
public void checkCanId_invalidCanIdTest() { | ||
// Can IDs can only valid in the range [0, 62]. | ||
int invalidCanIds[] = {-50, -1, 63, 100}; | ||
for (int invalidCanId : invalidCanIds) { | ||
var exception = | ||
assertThrows(InvalidCanIdException.class, () -> InputValidation.checkCanId(invalidCanId)); | ||
assertThat(exception) | ||
.hasCanId(invalidCanId) | ||
.hasMessageThat() | ||
.contains("is not a valid can id"); | ||
} | ||
} | ||
|
||
@Theory | ||
public void invalidCanID( | ||
@Between(first = -50, last = -1) @Between(first = 63, last = 100) int canID) { | ||
InvalidCanIdException expected = new InvalidCanIdException(canID); | ||
InvalidCanIdException actual = | ||
assertThrows(InvalidCanIdException.class, () -> InputValidation.checkCanId(canID)); | ||
assertEquals("Messages were not identical", expected, actual); | ||
} | ||
|
||
@Theory | ||
public void genericInvalidTest(@Between(first = 0, last = 4) int unused) { | ||
Integer min = Integer.valueOf(0); | ||
Integer actual = Integer.valueOf(unused); | ||
Integer max = Integer.valueOf(4); | ||
InputValidation.checkBounds(min, max, actual, InputValidationTest::outOfBounds); | ||
} | ||
|
||
@Theory | ||
public void validCanID(@Between(first = 0, last = 62) int canID) { | ||
assertEquals(canID, InputValidation.checkCanId(canID)); | ||
@Test | ||
public void checkCanId_validCanID() { | ||
// Can IDs can only valid in the range [0, 62]. | ||
int validCanIds[] = {0, 1, 10, 62}; | ||
for (int validCanId : validCanIds) { | ||
assertThat(InputValidation.checkCanId(validCanId)).isEqualTo(validCanId); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of prefixing with checkCanId
, you should create a static nested class if you want to differentiate between different kinds of input validation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though at the moment, we only have one
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea. Done.
int invalidCanIds[] = {-50, -1, 63, 100}; | ||
for (int invalidCanId : invalidCanIds) { | ||
var exception = | ||
assertThrows(InvalidCanIdException.class, () -> InputValidation.checkCanId(invalidCanId)); | ||
assertThat(exception) | ||
.hasCanId(invalidCanId) | ||
.hasMessageThat() | ||
.contains("is not a valid can id"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like how you are checking the exception, though the new testing without theories is a lot less thorough. With the previous implementation, roughly 100 can ids are checked, and this only checks 4 ids on the bounds. The performance overhead isn't big for checking a large number of ids; on my machine it takes 0.015 seconds to run it all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say there is no point in being thorough in this test. The nature of this function is so trivial that we can reasonably expect there won't be any surprises mid-range (e.g., 55 suddenly is an invalid CanID, while -10 suddenly is a valid CanID).
The questions I'd rather want us to answer are:
- Does this change improve readability? (when looked in isolation, this PR trades Theories for Subjects: hardly a readability win. But! if we establish that we stick with google Truth and thus with Subjects, we can reasonably expect that Prospect-Robotics maintainers should/will get accustomed to Subjects and we should avoid distracting them with other technologies in isolated tests)
- Do these unit-tests help a random maintainer get more easily a grasp of what
InputValidation.checkCanId
does? Do we improve their "documentation" value? Do the unit-tests serve as good "example usage" snippets?
@Test | ||
public void checkCanId_validCanID() { | ||
// Can IDs can only valid in the range [0, 62]. | ||
int validCanIds[] = {0, 1, 10, 62}; | ||
for (int validCanId : validCanIds) { | ||
assertThat(InputValidation.checkCanId(validCanId)).isEqualTo(validCanId); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same thing with other removal of Theories; this is a lot less thorough. With the previous test, the test would fail if any valid can id threw an exception
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my other comment. I don't think we need to thoroughly exhaust all possible valid/invalid values in this particular scenario.
private InputValidation() { | ||
throw new AssertionError("non instantiable"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably don't want to make the constructor public by deleting it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, this was an unintended change. Thanks for catching it!
@@ -52,6 +28,7 @@ static void checkBounds( | |||
* @return the {@code id} | |||
* @throws InvalidCanIdException if the id is invalid | |||
*/ | |||
@Deprecated // Not used anywhere in org:Prospect-Robotics |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we think the utility functions for checking bounds are useful, then we may want to start using it (and/or other bound checking utilities in this class). In other words, is this Deprecated because it is bad, or is it bad that we aren't using it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was a speculative change on my end, aiming to provoke a discussion if we should deprecate and then remove this function if we are not going to use it.
Granted, this is a general utilities library and we can keep functions around even if we don't currently use them. I'm removing the Deprecated for now then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method wasn't public, so the removal-vs-deprecate argument appears to be moot.
Agreed that this appears to be speculative.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Un-deprecated it for now.
assertThat(ControlUtils.deadband(-1.0, 0.5)).isEqualTo(-1.0); | ||
assertThat(ControlUtils.deadband(-0.75, 0.5)).isEqualTo(-0.5); | ||
assertThat(ControlUtils.deadband(0.6, 0.5)).isWithin(1e-9).of(0.2); | ||
assertThat(ControlUtils.deadband(0.75, 0.5)).isEqualTo(0.5); | ||
assertThat(ControlUtils.deadband(1.0, 0.5)).isEqualTo(1.0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since actual computations are being done with these calls, we may want to use calls to isWithin
for each of these. See google truth webpage
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, things look more consistent if we apply isWithin
everywhere. Done.
lib/build.gradle
Outdated
@@ -67,13 +67,23 @@ tasks.named('jar') { | |||
} | |||
} | |||
|
|||
// Generates Javadocs for implementation libraries under /build/docs/javadoc |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment is a little bit misleading, since gradle will make the javadoc go in the specified directory regardless of the existence of the following code; the code that follows this comment just configures some extra javadoc tags.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the documentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the review, @cuttestkittensrule !
Meta comment: here we have a good discussion around "Theories" vs "Truth" and we probably can get into quite a rabbit hole, each one proving their opinion is right :) On a high-level both versions are functionally ~equivalent, so the discussion boils down to, what I call, "matter of personal preference". And "matter of preferences" discussions are often the hardest to resolve.
As a code-owner and a SW lead you have the authority to continue upholding your opinion, your standards of quality, and your architecture philosophy and therefore you can continue requiring these to be met before you approve the PR. Or, and I'd like to implore you to consider this alternative for its broader effect on the team, you can let a developer who's just learning their ways in the code change things a bit more to their liking. I personally take the latter approach in my professional life because:
- It allows incoming developers to build a sense of shared agency to the code.
- It allows them to better familiarize themselves with the code and Java. There's hardly a better way to learn a code than when you are changing it - breaking it and fixing it in the process
One thing that's kind of ironical in this situation is that, if I were the code-owner, and you came around the next day proposing to implement a new unit-test with "Theories", I, following this same philosophy, will let you check it in. I'll then still come around a few months later and will propose: let's do another pass and see if we can simplify that code and remove the Theories. So, the end state is that we still don't allow Theories in the code, but we'd still benefit from doing this. With each round (adding Theories, then removing them) we learn something new and with each round we will have a discussion on whether or not the changes make sense.
lib/build.gradle
Outdated
@@ -67,13 +67,23 @@ tasks.named('jar') { | |||
} | |||
} | |||
|
|||
// Generates Javadocs for implementation libraries under /build/docs/javadoc |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the documentation.
private InputValidation() { | ||
throw new AssertionError("non instantiable"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, this was an unintended change. Thanks for catching it!
@@ -52,6 +28,7 @@ static void checkBounds( | |||
* @return the {@code id} | |||
* @throws InvalidCanIdException if the id is invalid | |||
*/ | |||
@Deprecated // Not used anywhere in org:Prospect-Robotics |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was a speculative change on my end, aiming to provoke a discussion if we should deprecate and then remove this function if we are not going to use it.
Granted, this is a general utilities library and we can keep functions around even if we don't currently use them. I'm removing the Deprecated for now then.
assertThat(ControlUtils.deadband(-1.0, 0.5)).isEqualTo(-1.0); | ||
assertThat(ControlUtils.deadband(-0.75, 0.5)).isEqualTo(-0.5); | ||
assertThat(ControlUtils.deadband(0.6, 0.5)).isWithin(1e-9).of(0.2); | ||
assertThat(ControlUtils.deadband(0.75, 0.5)).isEqualTo(0.5); | ||
assertThat(ControlUtils.deadband(1.0, 0.5)).isEqualTo(1.0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, things look more consistent if we apply isWithin
everywhere. Done.
int invalidCanIds[] = {-50, -1, 63, 100}; | ||
for (int invalidCanId : invalidCanIds) { | ||
var exception = | ||
assertThrows(InvalidCanIdException.class, () -> InputValidation.checkCanId(invalidCanId)); | ||
assertThat(exception) | ||
.hasCanId(invalidCanId) | ||
.hasMessageThat() | ||
.contains("is not a valid can id"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say there is no point in being thorough in this test. The nature of this function is so trivial that we can reasonably expect there won't be any surprises mid-range (e.g., 55 suddenly is an invalid CanID, while -10 suddenly is a valid CanID).
The questions I'd rather want us to answer are:
- Does this change improve readability? (when looked in isolation, this PR trades Theories for Subjects: hardly a readability win. But! if we establish that we stick with google Truth and thus with Subjects, we can reasonably expect that Prospect-Robotics maintainers should/will get accustomed to Subjects and we should avoid distracting them with other technologies in isolated tests)
- Do these unit-tests help a random maintainer get more easily a grasp of what
InputValidation.checkCanId
does? Do we improve their "documentation" value? Do the unit-tests serve as good "example usage" snippets?
@Test | ||
public void checkCanId_validCanID() { | ||
// Can IDs can only valid in the range [0, 62]. | ||
int validCanIds[] = {0, 1, 10, 62}; | ||
for (int validCanId : validCanIds) { | ||
assertThat(InputValidation.checkCanId(validCanId)).isEqualTo(validCanId); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my other comment. I don't think we need to thoroughly exhaust all possible valid/invalid values in this particular scenario.
@Test | ||
public void checkCanId_invalidCanIdTest() { | ||
// Can IDs can only valid in the range [0, 62]. | ||
int invalidCanIds[] = {-50, -1, 63, 100}; | ||
for (int invalidCanId : invalidCanIds) { | ||
var exception = | ||
assertThrows(InvalidCanIdException.class, () -> InputValidation.checkCanId(invalidCanId)); | ||
assertThat(exception) | ||
.hasCanId(invalidCanId) | ||
.hasMessageThat() | ||
.contains("is not a valid can id"); | ||
} | ||
} | ||
|
||
@Theory | ||
public void invalidCanID( | ||
@Between(first = -50, last = -1) @Between(first = 63, last = 100) int canID) { | ||
InvalidCanIdException expected = new InvalidCanIdException(canID); | ||
InvalidCanIdException actual = | ||
assertThrows(InvalidCanIdException.class, () -> InputValidation.checkCanId(canID)); | ||
assertEquals("Messages were not identical", expected, actual); | ||
} | ||
|
||
@Theory | ||
public void genericInvalidTest(@Between(first = 0, last = 4) int unused) { | ||
Integer min = Integer.valueOf(0); | ||
Integer actual = Integer.valueOf(unused); | ||
Integer max = Integer.valueOf(4); | ||
InputValidation.checkBounds(min, max, actual, InputValidationTest::outOfBounds); | ||
} | ||
|
||
@Theory | ||
public void validCanID(@Between(first = 0, last = 62) int canID) { | ||
assertEquals(canID, InputValidation.checkCanId(canID)); | ||
@Test | ||
public void checkCanId_validCanID() { | ||
// Can IDs can only valid in the range [0, 62]. | ||
int validCanIds[] = {0, 1, 10, 62}; | ||
for (int validCanId : validCanIds) { | ||
assertThat(InputValidation.checkCanId(validCanId)).isEqualTo(validCanId); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea. Done.
assertEquals(0.0, ControlUtils.deadband(-0.25, 0.5), 1e-9); | ||
assertEquals(0.0, ControlUtils.deadband(0.0, 0.5), 1e-9); | ||
assertEquals(0.0, ControlUtils.deadband(0.5, 0.5), 1e-9); | ||
assertThat(ControlUtils.deadband(-0.5, 0.5)).isWithin(1e-9).of(0.0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this test method and the deadbandZwroDeadbandHasNoEffect
, we actually don't want the isWithin
call. To quote Google Truth's webpage,
Suitable uses for exact equality include cases where the contract of the code under test specifies…
- …that it will copy values from the input to the output without doing any arithmetic on them.
- …that it will return fixed values specified as exact double or float values, such as class constants or literals.
Suitable uses for approximate equality include cases where the contract of the code under test specifies…
- …that it will do any kind of arithmetic.
- …that it will return fixed values specified as mathematical values, such as 1/10 or π.
The contract for this method is that if the input is within the deadband, exactly 0.0 will be returned, so not using isWithin
is appropriate for this test. For deadbandZwroDeadbandHasNoEffect
, the contract of the tested method also states the input is returned as-is with a deadband of zero, so not using isWithin
is appropriate. However, as per my prior comment, deadbandValuesOutsideDeadbandAreAdjusted
tests the modification of the input when it is outside of the deadband, and should use isWithin
because arithmetic is being performed.
Tldr; only deadbandValuesOutsideDeadbandAreAdjusted
should use isWithin
, because it is the only one of these three tests that tests a situation when the deadband method performs arithmetic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good plan. Done.
lib/src/test/java/com/team2813/lib2813/util/InvalidCanIdExceptionSubject.java
Outdated
Show resolved
Hide resolved
assertThat(exception) | ||
.hasCanId(invalidCanId) | ||
.hasMessageThat() | ||
.contains("is not a valid can id"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think more straight-forward assertions would be more readable, and would remove the need for a custom Truth subject.
Also it might be good to verify that the invalid value is included in the exception message.
assertThat(exception) | |
.hasCanId(invalidCanId) | |
.hasMessageThat() | |
.contains("is not a valid can id"); | |
assertThat(exception.getCanId()).isEqualTo(invalidCanId) | |
assertThat(exception) | |
.hasMessageThat() | |
.contains("not a valid can id"); | |
assertThat(exception) | |
.hasMessageThat() | |
.contains(String.valueOf(invalidCanId)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, this is indeed so much better to do here.
I have to admit that I myself got carried away with the Subjects here - I just got curious what does it take to implement one :)
// Can IDs can only valid in the range [0, 62]. | ||
int invalidCanIds[] = {-50, -1, 63, 100}; | ||
for (int invalidCanId : invalidCanIds) { | ||
var exception = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I think specifying the actual type here would allow readers that are not familiar with assertThrows()
to have a better intuition of what is going on here.
var exception = | |
InvalidCanIdException exception = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My thinking here was that the type is already set by the first argument in assertThrows(InvalidCanIdException.class ...)
here. So this is a situation where auto
would be recommended in current C++ style guides. But that's not C++, and the IDE throws under-wrinkles this as an error, so I'm probably just wrong here.
// Can IDs can only valid in the range [0, 62]. | ||
int validCanIds[] = {0, 1, 10, 62}; | ||
for (int validCanId : validCanIds) { | ||
assertThat(InputValidation.checkCanId(validCanId)).isEqualTo(validCanId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here it's a bit hard to see what code is actually being tested. Introducing a local and adding a blank line would make it easier to visually see what is being tested (and would be closer to the Arrange-Act-Assert pattern):
assertThat(InputValidation.checkCanId(validCanId)).isEqualTo(validCanId); | |
int returnValue = InputValidation.checkCanId(validCanId); | |
assertThat(returnValue).isEqualTo(validCanId); |
Note that if this were to fail, the test failure message wouldn't indicate which input value was provided (it would in the Theories-based version). You could do something like this:
assertThat(InputValidation.checkCanId(validCanId)).isEqualTo(validCanId); | |
int returnValue = InputValidation.checkCanId(validCanId); | |
assertWithMessage("%d should be considered a valid ID", validCanId) | |
.that(returnValue).isEqualTo(validCanId); |
I personally find the Theories-based version of this test to be extremely easy to read, but concede that having to create a custom ParameterSupplier
increases the cost of using Theories in this class. I'm not convinced that we should forbid use of Theories.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're probably right. I'd suggest, let me get it my way this time just so I can gain a little more confidence with this code-base. I should not be allowed to do obviously wrong things in the code, but I hope it should be OK for me (or any other qualified contributor) to reshape it here and there to their liking ...
lib/build.gradle
Outdated
source = sourceSets.test.allJava | ||
classpath = sourceSets.test.compileClasspath | ||
destinationDir = file("${buildDir}/docs/testjavadoc") | ||
println (title == null? '<null>' : title) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
println (title == null? '<null>' : title) | |
println (title == null ? '<null>' : title) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
As I mentioned on the Slack thread, Truth is an assertion framework; Theories is a runner (runners in JUnit 4 are the abstraction that runs tests). They have different goals. Specially in this PR, you didn't replace Theories with Truth. You did two things:
We can, of course, argue about the cost/benefits of Theories and Truth, but I find that framing it as "Theories" vs "Truth" to be confusing.
FWIW, in my professional life, I'm perfectly fine if people make the code easier to understand, reduce complexity, add clarifying comments, etc. I'm a bit hesitant to have people change between two equivalent coding styles (unless the previous style is uncommon or against best practices). My reasoning tends to be that at least two people already decided that the code was acceptable (the reviewer(s) and the original author) and changing code between styles doesn't add much value. I think the larger concern is how many concepts a new member of the team will have to learn to be effective at making changes in the code. There's always something to learn, so we shouldn't be surprised if there are libraries and concepts that senior members of the team haven't used, of course. Mr. Dikov's concern appears to be about how much time it takes for someone to be reasonably effective at making changes to some/most of the code. As I discovered when I started watching the Java training classes in the lab, there are a lot of concepts new people have to know just to understand Java. We don't (yet) teach WPILib, PathPlanner, use of AdvantageScope, Java testing or Git. We should certainly be asking ourselves whether the benefits of using the libraries we aren't required to use outweigh the learning curve for newer team members. |
…onsistency across unit-tests
…st to use Google Truth matchers instead of Theories; fix a bug - InvalidCanIdException was not capturing a failing canId what looked like the intent of this class
fbce70c
to
7915771
Compare
Thanks @kcooney and @cuttestkittensrule ! The PR is ready for another round. Please take another look! |
canId
, just a message about the out-of-band area