Don't listen to dogmas too much. Go for the simplest thing that could possibly work, also for tests. (Disclaimer: I do know TDD, but I don't know Objective C).
To let SieveClient
create its SaslConn
's in production code, but use mock ones in tests, you can use dependency injection. Add a setter method to SieveClient
to pass in a factory (as an object or a function, depending on what Objective C permits), which SieveClient
will use to make its SaslConn
's, instead of making them by itself. The test code provides a test factory that dishes out mocks. The production-case code for making SaslConn
's either moves to another factory to be unit-tested independently, or if it's too simple to break, remains as the default behavior inside SieveClient
when the factory setter is not called.
The simplest way to test network client code is by far to implement or re-use a mock server. Don't mock out the gory socket details in SaslConn
; instead, write an SASL server in your tests. The fact that your SaslConn
can talk to it goes a long way towards providing testing for that mock server; in other words, SaslConn
and the mock server are each other's unit tests. (Yeah, not "unit" in the purist sense, but nobody cares.)
Finally, I have mixed feelings about the precept that hard to test code is badly designed. It depends. You should design your code so that it's easy to use (in caller code) and easy to modify. Unit tests are but a means to these ends: they are the first caller code that you will write, and they give you confidence that you don't screw up when making changes. Don't let a particular framework or methodology twist and maim your design to the point of outweighing the benefits of TDD. In particular, expectation-based mocking frameworks such as OCMock make it way too easy to write brittle tests that go like "I expect method foo
to be called 3 times, and only then method bar
to be called with exactly such and such arguments". Rather than using the wrong tools for the job, write your own!