-
Notifications
You must be signed in to change notification settings - Fork 11
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
Functional tests #14
Functional tests #14
Conversation
The coverage thing turned out to be pretty easy. I enabled coveralls for my fork (see https://coveralls.io/github/jerith/slacko for the results) but it'll have to be enabled separately for the main repo. Adding test runs to the builds seems to make them significantly slower. :-( |
These tests would be much easier to work with if there were a good way of turning errors back into strings (which there is already an issue for: #4). I'm starting to uncover some bugs with these tests. I don't really want to fix them in this branch, because the focus here is on writing tests for the existing code, so I'll make a note of them in the PR description above (where they'll be more visible than buried in the comments) so they can be addressed separately. |
34bfe65
to
14450ca
Compare
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 this is a very promising approach and apart from the mutable bit that I'd like to avoid it looks very nice. Looking forward to your input.
src/slacko.ml
Outdated
@@ -584,10 +584,13 @@ type history_result = [ | |||
| timestamp_error | |||
] | |||
|
|||
let base_url = "https://slack.com/api/" | |||
let base_url = ref "https://slack.com/api/" |
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 we whould turn the module into a functor and split it into a inner module Config
(though not sure what to really name it) module which contains stuff like base_url
and an instantiation of the module with the default config module applied, so users will retain their API while we make parts modular and the test can therefore instantiate the functior with a different Config
.
The advantages I see here is that this inner module can also hold other information like a cache of username/channel etc to it's ID and would be localized to a base_url
. This is why I am not super sure with the name Config
as it could be used for other useful things which do not map to a config.
Let me know what you think.
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 really like the thought of tying state to a module, because it makes it harder to (for example) talk to multiple slacks with different tokens.
What about something like this:
type session = {
base_url : string;
token : token;
mutable cache : some_suitable_type; (* maybe the value itself can be mutable instead *)
}
let create_session ?(base_url="https://slack.com/api/") token = {
base_url;
token = token_of_string token;
cache = empty_cache; (* or something similar *)
}
We make the session
type abstract and we use it everywhere we're currently using token
. We can also turn token_of_string
into a wrapper that creates a new session and keep the external API the same for everything except functions that talk to slack and don't currently need a token (which I believe is only api_test
and oauth_access
).
I'm happy to prototype both this and the functor version you suggested (in separate branches) and see which one works better.
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 hacked up a proof of concept for each version.
Functor
Branch: https://github.com/jerith/slacko/tree/functor-based
Comparison to master: master...jerith:functor-based
This doesn't change anything in the existing public API (except to add stuff) but it shifts an awful lot of code around. (I didn't reindent the module signature or the code in the functor, otherwise the diff would be rather bigger.)
Choosing whether to use the Slacko
module or a custom-built version wrapping a different base_url
at runtime got a bit awkward, so I just made a copy of slack_notify
that takes a mandatory --base-url
parameter as an example of the functor's use.
Session value
Branch: https://github.com/jerith/slacko/tree/session-based
Comparison to master: master...jerith:session-based
This changes the signature of all functions that take a token
and has them take a session
instead. Like token
, session
is an abstract type. token_of_string
now returns a session
instead, but you need to use make_session
if you want to (optionally) provide a base_url
value. The two API functions that didn't take a token (api_test
and oauth_access
) now accept an optional ?base_url
parameter.
I added a --base-url
option (which does the obvious thing) to slack_notify
to test the session stuff.
Thoughts
I compiled my slackobot code (unmodified) against both versions to verify backwards compatibility (at least for the parts of the API I'm using). I also modified my code to use the session value version explicitly (which was very straightforward) and attempted to do the same for the functor version, but I had forgotten to export the Sig
module and thus couldn't use the functor. The my_slack_notify
code gave me a feel for using that anyway, so I didn't bother fixing the problem.
While I prefer the session value version, I'm happy to take whichever you're happier with and open a pull request with a cleaned up implementation, etc.
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.
Thank you for the comprehensive comparison. I have thought a bit about this and agree with your conclusion.
I do not mind as much breaking the API (we are still 0.x so figuring out a decent API is completely okay) nor the code changes in the functor approach but it's true that the functor does not really bring any advantage from the perspective of types, it's just more of a hassle to be able to use it.
src/slacko.mli
Outdated
|
||
(**/**) | ||
(** Sets the base URL to make API requests to. For use in tests. *) | ||
val set_base_url : string -> unit |
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.
With an inner module, we might not even need to have a function like this. Though ocamldoc probably will output documentation for the unapplied functor, which regular users should not need to care about.
@@ -0,0 +1,73 @@ | |||
(* Slacko's public interface doesn't let us easily construct Slacko.user and |
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.
Why do you need to extract values from these types? To do comparisons? I feel this is a bit brittle, as the types now have to be declared effectively twice. Correct me if I am misunderstanding.
Another idea: adding comparison functions to Slacko which compare some (relevant) values of the records without exposing the inner workings.
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.
The tests need to build expected values to compare the actual values to. I was initially just using channel_of_string
and user_of_string
, but both of those explode on empty strings and there are places in the API the return empty strings for user
fields.
This abbrtypes
thing definitely needs to be replaced with something better -- it's very much a temporary hack to let me move on with the more useful test code without having to solve this problem properly first. :-)
test/fake_slack.ml
Outdated
| _ -> failwith "Can't parse test json." | ||
|
||
|
||
let resp ok fields = |
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.
Picky, but I'd rather like this to have a proper name.
test/fake_slack.ml
Outdated
|
||
let check_auth f req body = | ||
match get_arg "token" req with | ||
| "xoxp-testtoken" -> f req body |
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 be nice to have these magic constants factored out with understandable names like valid_test_token
in this case, so the reader knows what this branch represents.
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.
fake_slack
is currently very specific to the test data it's working with. It probably needs to become more general, but that ties into the difference between unit/functional/integration tests and how we handle the much less predictable data we get when talking to the real slack API.
test/fake_slack.ml
Outdated
|
||
let channels_archive req body = | ||
match get_arg "channel" req with | ||
| "C3UK9TS3C" -> err_resp "cant_archive_general" [] |
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 here, general_channel
, regular_channel
and archived_channel
come to mind.
test/test_slacko.ml
Outdated
open Abbrtypes | ||
|
||
|
||
let token_str = |
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 just match on exception. I think we do that somewhere anyway (and depend on a recent enough OCaml version) and I think it looks nice and coherent.
test/test_slacko.ml
Outdated
let () = match token_str with | ||
| "xoxp-testtoken" -> Slacko.set_base_url "http://127.0.0.1:7357/api/" | ||
| _ -> | ||
print_endline ("NOTE: Because an API token has been provided, " ^ |
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.
These notes are a nice idea, though should probably go on stderr.
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.
They should, thanks.
test/test_slacko.ml
Outdated
|
||
let test_api_test_nodata tctx = | ||
Slacko.api_test () >|= get_success >|= fun json -> | ||
assert_equal ~printer:Yojson.Safe.to_string |
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 it uses an already provided printer. Printing things can be so painful sometimes.
14450ca
to
c8c84c7
Compare
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.
Turns out the current cohttp depends on a version of conduit we can use.
opam
Outdated
"ounit" {test} | ||
# conduit is pulled in by cohttp, but for tests we need the server stop | ||
# fixes in 0.14.0 | ||
"conduit" {test & >= "0.14.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.
cohttp
0.21.1 (released only a few days ago) has a dependency on conduit
≥ 0.14.0, so I suggest bumping that dependency.
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.
Glad to see progress on this and I think it goes in the right diretion.
How about this: let's get this into a "releaseable" shape, since the last release is very old and publish it. Then iterate on writing tests and fixing what is broken. The downside is that the new release might work worse than the old version, which assumed less of what Slack was returning.
test/fake_slack.ml
Outdated
open Cohttp_lwt_unix | ||
|
||
|
||
let valid_token = "xoxp-testtoken" (* The only "valid" token we accept. *) |
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.
Can you put this comment above the line? This prevents lines from getting too unwieldy long and ugly.
let new_channel_json = Yojson.Safe.from_file "new_channel.json" | ||
let authed_json = Yojson.Safe.from_file "authed.json" | ||
let random_history_json = Yojson.Safe.from_file "random_history.json" | ||
let users_json = Yojson.Safe.from_file "users.json" |
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's an idea: instead of specifying all JSON, maybe it would be possible to write a minimal stub which can generate a fake Slack endpoint by looking up <endpoint_name>.json
in tests/
and returning that.
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.
There isn't really enough commonality between the different endpoints to make that work easily. For some of them (such as the history APIs), we'll also need some more complex logic to decide what to return.
I'm hoping to eventually replace this with a more generic test double that doesn't rely on a bunch of stored data, but that's too big for this PR.
test/fake_slack.ml
Outdated
|
||
|
||
let reply_json ok fields = | ||
let body = `Assoc (("ok", `Bool ok) :: fields) |> Yojson.Safe.to_string in |
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 would prefer |>
to be in it's own line, like in slack.ml
.
That's pretty much where I was headed too. I'd like to at least cover the issues from #10 before merging, but that shouldn't take too much work. |
#18, #19, and #20 fix all the problems that currently require tests to be skipped in this branch. Is there anything else you'd particularly like tests for in this branch? |
This is the start of my attempt at building a suite of functional tests for slacko. It's nowhere near ready to merge, but it's a reasonable starting point for a conversation about what we'd like this to become.
The only change to the slacko code itself is that
base_url
is now a reference and a newset_base_url
function (which I've told ocamldoc to leave out) allows it to be set externally.I haven't quite figured out how to handle the tension between strict test cases that make use of known data and the much more relaxed tests that can be run against the real API to make sure slacko continues to work properly as the slack API evolves over time.
For now, I plan to add simple tests around a few more functions
and maybe figure out how to generate test coverage reports(coverage is done).Here are some problems the tests have already uncovered:
Channel_not_found
, masking legitimate responses likeInvalid_auth
.channels.create
contains"0000000000.000000"
in thelast_read
field while we expect atimestamp
/float
. I don't know if this is a special case or if this field is always string-encoded like this --channels.list
doesn't return that field.I'd prefer to address these problems in separate branches and keep this one focused on the tests themselves, but it would probably make sense to merge or rebase some of the fixes back into this branch.