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
add: unit test for textutils issue #196 #204
Conversation
- existing code is working but this verifies it - had to change to class methods because android unit tests require mocking out android library calls and this is not obvious (or possible?) with static. impact on callers is minimal at least.
@srsudar @batkinson Since you two are the testing experts, can you take a quick look at Lindsay's work? |
Not sure I'm worthy of that mantle, but I can take a look this weekend. |
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 don't know if the approach you used will be consistent with the testing conventions for Collect going forward, but the fact you took the initiative to create the test is awesome. Thank you.
@@ -4,7 +4,7 @@ buildscript { | |||
jcenter() | |||
} | |||
dependencies { | |||
classpath 'com.android.tools.build:gradle:2.1.0' | |||
classpath 'com.android.tools.build:gradle:2.1.3' |
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.
Commit message should describe the commit contents comprehensively. Updating the gradle tools version should not be part of this commit.
public Spanned toHtml(String text) { | ||
return Html.fromHtml(text); | ||
} | ||
} |
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.
Changing the API like this smells unless we intend to make it a convention for similar. I suspect we would use Roboelectric or make it an instrumentation test.
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 might be a good occasion to continue the discussion on our testing frameworks. This is the kind of thing I would use Robolectric for, but I'm not sure how the group feels about it. I'll post back there about it, as the answer will impact how Lindsay modifies the PR.
zipStoreBase=GRADLE_USER_HOME | ||
zipStorePath=wrapper/dists | ||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-all.zip | ||
#Wed Sep 21 15:07:15 AEST 2016 |
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.
Again, commit contents should be apparent from description. Updating the gradle wrapper should not be part of it.
verify(mHtml).toHtml(captor.capture()); | ||
String input_arg = captor.getValue(); | ||
assertEquals(input, input_arg); | ||
} |
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.
Nice and concise test.
Thanks for reviewing this and the helpful comments. Robolectric seems to have done the trick, so I've rolled back the changes to the main classes. I was wondering (maybe best for a new ticket) but is there any point to the 4 product flavors (demo, master, snapshot, uitest)? They seem to be identical and just causing the CI build to take 4 times longer. |
@lindsay-stevens The 4 product build flavors exist because Collect currently shares its gradle configuration with the ODK 2.0 tool suite. ODK 2.0 uses the 3 of the build flavors to generate different binaries (Snapshot = new binary for each commit, Demo = synchronized stable points, Master = full release), and the last flavor (uitest) for separating out tests that require graphics rendering (our build server runs headless). Collect is not currently part of that workflow, so it is possible to remove those flavors if there is community will to do it. I do not anticipate it being a lot of work, but we'd want to check against the common gradle config in this repository: https://github.com/opendatakit/gradle-config/ |
Jeff, what does Collect depend on in that common repo? Does Collect depend on any shared libraries? I'm not too familiar with shared gradle configs, so this might be an obvious question if you know where to look in that repo. |
public void textToHtml_CallsHTMLOnRealInput() { | ||
String input = "This *bold* markdown is \n- nice,\n- efficient.\n"; | ||
CharSequence result = TextUtils.textToHtml(input); | ||
assertNotNull(result); |
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 LOVE that we are adding tests, and a huge kudos to you for doing so. I feel like with the change from the original commit (which had the captor
business), however, that this method name is no longer appropriate. Right now it's just asserting that the returned result is not null, not that it actually calls the value on the input. Someone could rewrite textToHtml
to just return an empty string and this test would still pass.
I think that a better test for this would be something like:
@Test
public void textToHtml_CallsHTMLOnRealInput()
String arg = "bar";
String expected = "supposedly styled string";
TextUtils tuMock = mock(TextUtils.class);
when(tuMock.textToHtml(eq(arg))).thenReturn(expected);
String actual = tuMock.textToHtml(arg);
verify(tuMock, times(1)).textToHtml(eq(arg));
assertEquals(actual, expected);
}
There I'm trying to assert both that the method was called with the argument expected and that we are returning the result of the textToHtml
method. The unfortunate thing is that all the methods are static as written. I agree with your original sentiment that this is ugly. I'd vote that these methods either change to non-static and we test like above, or we just change the name of the test to better describe what it's actually doing.
Adding a test framework is a big win, and I'd LGTM a PR like this that lays the groundwork for more tests even if it doesn't add a lot of tests itself.
edit: updated some typos from some local tests I was running.
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 @srsudar I think that's a very sensible change. I wanted to do something like that but wasn't sure what the best approach might be. Maybe if @batkinson could confirm I'll update the PR for merge?
Re: gradle configs: maybe we can address this by only running one flavour in CI for collect. I'd imagine there is a way to set that as a build parameter in circle.yml, which I've opened a new ticket for adding (#205) once we're set on this PR.
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.
@lindsay-stevens I appreciate the sentiment, but I can't claim special expertise over @srsudar. However, after looking closely at the code he commented on, I agree with his sentiment (especially with establishing the testing foundation in the code base), but I am not sure I like the alternative offered any better.
My major complaint is that a good test usually makes the issue involved clear. To gauge the relevance of the test I had to reference #196. Unfortunately, the issue isn't clear on the root problem either. So, we (as will others in the future) have to lean on the test code. A good test method name consistent with a concise implementation should be enough. Sam's re-implementation brings the method contents back into alignment with the method name, but the method name does not help to understand the value of the test (other than a html method is called).
I'm still foggy on the purpose of the test, but is there a reason we're diving into the guts of the implementation? For example, could we create a focused test for the defect like so?
@Test
public void singleMarkdownReplacement()
String input = "This *bold* markdown is \n- nice,\n- efficient.\n";
assertEquals("unexpected markdown-to-html translation", "This <b>bold</b> markdown is \n- nice,\n- efficient.\n", TextUtils.textToHtml(input));
}
Obviously, this test does not help to understand #196, but it is a clear test and it is stable with a change in implementation since we treat the method as a black box. If you have a specific test case and a good description that uniquely corners #196 like this, it would be a significant improvement. If I get a chance, I will try to come up with one myself.
Let me know if my feedback isn't clear.
Fair points. Thanks for your patience with this.
I guess the reason for the test is that since this TextToHtml function is
more or less the class's interface, and it doesn't declare that it throws
any exceptions, the test nails down the expectation that at least it won't
throw an NPE on null input (which is what caused #196).
Maybe a more precise test would focus on that, with an assertNotRaises type
of test?
As far as I can tell it's not possible to actually test the input / output
because the Android library functions can't be used in unit tests and
robolectric only provides fakes / stubs. If there's some way to do that I
think it'd be another useful test though, so there's something testing what
the class is expected to produce.
|
My understanding after reading #196 was that certain inputs caused it to As for the test feedback I offered, my apologies. I have not used On Mon, Sep 26, 2016 at 10:38 AM, Lindsay Stevens notifications@github.com
|
I don't think the problem in this case is actually with robolectric but rather with how we're trying to test the output. As a general rule, robolectric shouldn't just provide fakes/stubs. It generally calls through to either the real Android implementation or a (sometimes imperfect) re-implementation. For example, it looks like they support the I believe the reason @batkinson's test is failing is that the result of Looking out the output from Brent's test, I think the method is actually behaving correctly. Rather than:
the text is being converted first to this markdown:
and then a
So the assertion is failing but I actually think it's working as expected. You can plug this code into
So, that's a roundabout way of saying that in this case I don't think robolectric is the problem. If we want to test the API rather than the implementation (which in general is of course the way to go), I think we should move |
Thanks Sam. That explains the output I was seeing, and brings my view of On Mon, Sep 26, 2016 at 1:05 PM, Sam Sudar notifications@github.com wrote:
|
Sounds like we have consensus. @lindsay-stevens Can make the changes @srsudar suggested? And if those changes get @batkinson's LGTM, I think we'd be ready to merge and do a release. |
Sorry it took a little while to address this, I was away for a week. I think this is probably the best we can do in terms of testing the interface, as @srsudar detailed. Also I realised while writing the descriptions that #196 isn't exactly what this is all about. I think I just saw "TextUtils" in the description and assumed it was the same problem. In fact it's the NPE issue I mentioned over in getodk/getodk#1247. |
* In the real app this call would produce the following string, but | ||
* because the implementation uses android.Html.fromHtml, we end up with | ||
* robolectric's implementation of fromHtml which isn't exactly the same. | ||
* See opendatakit/collect#204. |
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 isn't quite right, I don't think. My comment in the discussion tries to explain why. Robolectric is doing the right thing. TextUtils.textToHtml()
is returning a Spannable
with HTML-like tag styling applied, it isn't converting markdown to HTML tags.
The "Correct with a capital C" way to test this method, I think, would be to remove the static
from these methods, mock out markdownToHtml
, and assert that textToHtml
passes its input to markdownToHtml
and returns the result of that method.
Is that something you're willing/able to do? If not, I would be ok removing this method and just keeping the textToHtml_BouncesNullInput
method, as it would be a nice simple way to add a testing framework and is the functionality you started this PR to test.
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 read over your earlier comment and my understanding is that if we nest some debug code inside an activity and run it (thereby calling the Android implementation), we get a HTML styled string in the logs:
"This <b>bold</b> markdown is \n- nice,\n- efficient.\n"
But in the context of this test (thereby calling the Robolectric implementation), we get a string that's not quite the same:
"This bold markdown is\n\n- nice,\n\n- efficient.\n\n"
Which is what I'm trying to convey in the test comment.
Either way, I think it's clear that it's not possible to perfectly test this class' interface as it is right now. But we can at least establish that the crash described in getodk/getodk#1247 is fixed, which is the goal of this PR. The criteria for that is that null input doesn't cause a crash, and that in the course of meeting that requirement we retain normal behaviour: non-null input causes some kind of Markdown to HTML processing to happen (disregarding the nature / correctness of that processing).
I agree this all could / should be a cleaner, but as you say it requires so rework of the TextUtils class which would be something for a separate PR. That might end up involving reworking of one or both of these tests, but who knows what will happen when we actually try to do 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.
I think that my confusing wording and variables names made that unclear. Sorry. The output of markdownToHtml
is the <b>bold</b>
bit. textToHtml
converts HTML tags to a styled Spannable
, which when logged loses the styling. So I think that Robolectric is behaving the same way the Android platform does.
I agree however that this is really neither here nor there. I'd be fine removing this test altogether, leaving only the non-null test, which as you say was the point of the PR to begin with.
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.
Let's not let perfect be the enemy of good. @lindsay-stevens remove the HTML test and then I'll be happy to merge.
- properly testing md/HTML processing requires some refactoring - dev team prefers to not have a superficial test for it in the meantime
android library calls and this is not obvious (or possible?) with static. impact
on callers is minimal at least.
If there is a better way to do this please advise. I haven't done much Java (mostly Python atm) so the way this all works is a bit opaque to me.