The feedback that comes immediately to mind: * it sounds like property #3 complects black box vs white box and specification vs implementation concerns * it sounds like the feature functions potentially combine the state and calculation layers in ways that should be internally reused, but also are externally exposed through a public API. These two needs should probably be decoupled * it should be an implementation detail of X that X calls Y. This is just a convenience. From a blackbox perspective, all that X's callers care about is that X's contract is satisfied. They don't know and shouldn't care that this is done by calling Y. Satisfying this behavior could be done by calling Y, or calling a Y', or some other internal magic. The knowledge that it currently is done by calling Y is irrelevant to the perspective of satisfying a contract and shouldn't be reflected in testing that contract * that said, it sounds like Y, contractually, is actually a subset of X, a shortcut, to be used when the extra conditions that require X don't exist. So testing Y should be able to be done through X's tests.
Some suggestions: * it sounds like there should be another internal layer for reusable combinations of state and calculation functionality, and Y's implementation should get moved in there. Then Y, as a public API, is mostly just a wrapper of that fn. X, as a public API, should also call that library fn, not Y. * that library fn can be tested in a whitebox fashion, with-redefs to mock out the underlying internal functions so the unique behaviors are exercised * for blackbox API testing, since it sounds like X and Y have specification/contractual details in common, and Y may satisfy callers of X who have simpler conditions- the 25 tests for X are needed, but the 5 of those that apply when the extra X requirements are null can simply be repurposed against the Y endpoint- as long as that's what the contracts say. Hope that helps/makes sense, apologies if it misinterprets the question. On Sat, Apr 6, 2013 at 8:42 PM, Steven Degutis <sbdegu...@gmail.com> wrote: > Disclaimer: this isn't strictly about Clojure, more about semi-functional > programming techniques, which Clojure excels at. (Plus I'm using Clojure > for it.) > > Lately I've been experimenting with ditching the conventional MVC approach > to writing a web app. > > Now controllers are just dead-simple functions that translate HTTP to/from > our feature functions (or business logic functions). Each of these feature > functions do some or all of these things: > > 1) change some persisted state > 2) return result of calculations on persisted and/or given data > 3) call another feature function > > Functions often build upon other functions. This leads to a sort of > pyramid, where the entry-point of the feature (usually called by a > controller) is the tip, and the most primitive functions are at the bottom. > > Testing each individual function for behaviors #1 and #2 is > straightforward. But I'm running into a problem testing behavior #3. > > One way is to use `with-redefs`. This technique would be used at every > level along the pyramid (except the bottom level which doesn't call > anything else). > > This feels very fragile. For one thing, if any of the function signatures > change, any tests that reference that function have to change too, or > they'll become false positives, no longer representing the real world, just > the imaginary world that was setup in the test. > > Another way is to test state changes and return values at every level in > the pyramid. This represents the real world more, because if any feature > function changes, all the tests above it will fail. But there's two > problems with this. First, there's going to be a lot of redundancy in test > data in the vertical slice of the pyramid for a single feature. I'm on the > fence of whether that's really a problem. Secondly and more importantly, > each feature function's tests will also have to cover all the behaviors of > the functions it calls, besides testing its own behaviors. > > For example, X calls Y. X has 5 behaviors and Y has 5 behaviors. So Y will > have 5 tests, but X will have 25. This is because, from a high level, we > only care about the feature as a whole (a.k.a. X) and Y is only an > auxiliary function to assist X. We, the stakeholders of feature X, don't > know or care that Y exists. So Y's tests are irrelevant to us, who just > want to know that X does its job. So we want to see all of X and Y's > behaviors tested, without knowing about Y, to give us confidence that X > does all 25 things. But we still need to be responsible and test Y, > especially because X delegates half his work to Y in the name of healthy > abstraction. > > A third way is to sprinkle small amounts of Y's behavior into some or all > of X's tests. This relies heavily on probability, suggesting that X is > probably using Y because he shares at least one of Y's behaviors. If it's > true, then we know that X shares all of Y's behaviors. It becomes a sort of > "linked list" of features, with the sprinkled-in behavior of Y acting like > a "next" pointer. The downside is that there's a pretty fair chance that I > didn't really call Y, but I just copied/pasted some of his behavior. Sure, > not immediately after writing the test. But 6 months in the future, when > I've forgotten why I wrote the test this way, and I see that X is calling Y > when the test says he just wants to use a fifth of Y's behavior, I'll think > it's overkill and rip out the call to Y, replacing it with just whatever > X's test specifies. Then it just became another deadly false positive. > > These are all the testing techniques I can think of for #3. Maybe there's > more that I just don't know about. > > What do you think? What approach would you take in this situation? > > -- > -- > You received this message because you are subscribed to the Google > Groups "Clojure" group. > To post to this group, send email to clojure@googlegroups.com > Note that posts from new members are moderated - please be patient with > your first post. > To unsubscribe from this group, send email to > clojure+unsubscr...@googlegroups.com > For more options, visit this group at > http://groups.google.com/group/clojure?hl=en > --- > You received this message because you are subscribed to the Google Groups > "Clojure" group. > To unsubscribe from this group and stop receiving emails from it, send an > email to clojure+unsubscr...@googlegroups.com. > For more options, visit https://groups.google.com/groups/opt_out. > > > -- -- You received this message because you are subscribed to the Google Groups "Clojure" group. To post to this group, send email to clojure@googlegroups.com Note that posts from new members are moderated - please be patient with your first post. To unsubscribe from this group, send email to clojure+unsubscr...@googlegroups.com For more options, visit this group at http://groups.google.com/group/clojure?hl=en --- You received this message because you are subscribed to the Google Groups "Clojure" group. To unsubscribe from this group and stop receiving emails from it, send an email to clojure+unsubscr...@googlegroups.com. For more options, visit https://groups.google.com/groups/opt_out.