back

by jjgreen·8y ago·view on hn ↗
I found the first point on unit-testing of C odd, I always treat static functions as implementation details and test only the public interface. CUnit is nice enough (if a bit long in the tooth).
4 comments
Rust is ambivalent about it. You can put tests in external files and force yourself to test only the public interface if you want.

But you can also throw a `#[test]` function alongside the implementation. It's sometimes super convenient to test a leaf helper function directly instead of mocking a whole program around it. And because the test lives next to the implementation, it's easy to change or delete it when the impl changes.

There's another nice side-effect of having one standard test framework - `cargo test` can test any Rust program. The Rust team can automatically test new compiler releases against all known Rust code: https://github.com/rust-lang-nursery/crater

Generally speaking, yes, but it's nice to have the option. I've often encountered situations where writing tests is a good part of either developing a complex system, or debugging it when it goes wrong.

As a random example, say you're making a data-structure of some sort, and you need to write some internal mechanism to mutate it (balancing a binary tree, resizing a hash table, whatever). If that internal mechanism is somewhat complicated, or gets called many times in public call (or even recusively), it's nice to be able to write tests for it specifically, to make sure you got this critical piece of the puzzle right.

Writing a test for just the public interface would probably catch that SOMETHING is wrong, but not what part of the code. It's basically a version of sprinkling your code with assertions to make sure you've got your invariants covered.

It's fairly trivial to have every listing contain a main() gated on a testing ifdef which may call the local static functions. In essence every .c listing becomes a test program.
There's a culture of testers who believe 100% code coverage is important enough to include "private" implementations, and will happily break encapsulation rules for sake of hitting 100% coverage.
Eh, I think both extremes are sub-optimal.

Some small public APIs are backed by large enough implementations that it pays off to be able to test implementation details. Sure, it might be "poorly factored" code that should have a bigger API and smaller guts, but that's not always an something you can change. Also, writing tests for internal behavior before refactoring can give you a good blueprint for how the refactored code should behave--being able to read the tests to specify unclear behavior is, while far from enjoyable in some cases, better than nothing.

You're right that there are some pretty silly test suites that break encapsulation for a coverage number without actually testing anything useful, though.

I don't think an absolute "all tests must behave thus" rule (e.g. coverage requirements, "only test public functionality", "refactor the instant something isn't easily testable") is useful. Explain the benefits of each path, and make sure the decision of what compromise to make--and in any project more than a one-developer hobby, you will have to compromise here eventually--is in the hands of people with the experience and common sense to make the right one.

If things are really gnarly in the implementation, one can always break it up into submodules to expose that functionality as public (and that's usually good architecturally IMHO).