Integration tests would mix multiple modules.
Depending upon the specifics of your application, unit tests at a lower level than the module level may be suitable. E.g. complicated algorithms a module might internally rely upon.
Our language around testing is missing a slot. We have unit, integration, and end-to-end. Should be unit, module, multi-module, and end-to-end. Depending upon the specifics of the application, you might have more unit or module level tests.
A great semantic insight.
I still prefer to test complicated algorithms via the external interface. It does end up requiring some scaffolding code for generating inputs based on test cases, but code is for automating repetitive tasks, and they're pretty fast these days, so it's rarely a problem, even for many thousands of test variations. It enables you to test the interactions between your various unit-level variations, which is more likely to catch errors. Again, it also helps make your tests visible, along with the edge cases you're testing, and many times developers working at the unit test level are way more thorough than they actually need to be. Couple this with efficiencies around refactoring and it can actually save time.
An example: Let’s say you’re writing a DNS resolver.
You need to parse all of the various RDATA portions of records. It’s far easier to write an exhaustive suite of unit tests that verify the code is parsing those data elements properly and throw in many tests for edge-cases that might mean insecure software if done incorrectly.
But it’s absolutely invaluable that we also have integration tests that verify an entire Message parses, and even moves through the entire stack from parsing to response. But why construct an entire Message when all you need to test is the RDATA? It’s easier and less complex test code to localize the RDATA test, and then to do a smaller set of Message based tests that verify the message moves through all the stacks properly.
Then popping all the way up the stack, you actually need to generate network traffic and send real DNS queries and verify that those work. That means multiple processes in your end-to-end test. Trying to produce a full set of tests that would validate individual edges cases in all RDATAs would be an extreme amount of redundant code, and also would fail due to many other reasons not related to an RDATA change, thus making it more time consuming to track down a failure.
Each area of tests is absolutely important and all are necessary for different reasons.
http://www.ox.ac.uk/sites/files/oxford/styles/ow_medium_feat...
You need a thin layer of high-level tests (controller and/or browser) covering the entire application, and then columns of lower-level (integration and unit) diving deeper into it where there are areas of particular complexity or risk.