4 ms·
When it comes to testing, I now follow the advice of Gary Bernhardt's presentation, Functional Core, Imperative Shell: https://www.destroyallsoftware.com/screen
by bigmanwalter 6y ago
When it comes to testing, I now follow the advice of Gary Bernhardt's presentation, Functional Core, Imperative Shell: https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell https://www.destroyallsoftware.com/screencasts/catalog/funct...
The idea is to move all the logic of your app into pure functions that can be tested on the order of milliseconds. When you refactor your code to allow for this, everything just makes more sense. Tests can be run more often and you are far more confident about the behaviour of your code.
- mceachen 6y agoIt's excellent advice, but you still need to test that the wiring of all those functions works as well. As an example, PhotoStructure has over 4,000 tests which test isolated functions: classical "unit" tests, which run under a minute. There are only a few hundred integration tests, but those take a couple minutes. All of these tests are run on every commit across all supported OSes using GitLab's CI and self-hosted runners.
- 8192kjshad09- 6y agoMaybe I just don't get it, but this design seems impractical for systems that use the network alot. I work on an app whose basic structure is 1. Receive data from a webhook (not controlled by me) 2. Make some HTTP requests based on data from 1 3. Send data to another HTTP API The core of the logic is in step 2. How can I possibly have a functional core here? Step 2s behavior completely depends on the HTTP responses.
- bigmanwalter 6y agoPardon my pseudo-JavaScript, but given an event handler like this: const handler = ( message, data, req=superagent.post ) => { if (message === “a”) return req(“/api/a”, data) if (message === “b”) return {label:”b”, value: req(“/api/b”)} } By supplying the http client using the default argument on line 4, the client can be replaced when testing with a mock handler: test(“handler requests /api/a”, t=> t.assertDeepEqual( handler(“a”, {b:”c”}, (url, body)=>[url, body]) [“/api/a”, {b:”c”}] ) ) test(“handler requests /api/b”, t=> t.assertDeepEqual( handler(“b”, {b:”c”}, (url, body)=>[url, body]) {label:”b”, value:[“/api/b”, {b:”c”}]} ) ) You could also write test cases that handle multiple steps, complex transformations, and so on. It’s really nice being able to specify plainly the inputs and expected outputs of your functions and testing against that! It may not guarantee that everything behaves as the real system but it will catch 100% of your logic errors.
- sidlls 6y agoHardcore test-all-the-things people would likely tell you to break whatever is in step 2 up into functions such that the webhook data are passed as parameters into a function, that all the HTTP request/response work is encapsulated into a separate function, and that the function under test should make calls to these. Really hardcore ones might even suggest having separate functions for all these things, each of which can be tested as a single unit. These people are, in my view, making a categorical error (that more testing of this sort means higher confidence and better quality code).
- jzoch 6y agoThe idea is that your server handler mimics those steps and contains all the IO you need (aka external depdendencies). The receiving and sending of the http requests + any other I/O you need should be immediately visible in the top level method (in a simple model). Everything below is functional and pure. Take a request, use pure functions to transform and reshape the data, pass that back up the call-stack to the top level method. Do some I/O operation against the database, go into functional land to shape it a bit, bring it back up, combine with other data in a pure way and then finally send the response.