In Testing Without Mocks, James Shore describes an architecture pattern he calls the A-Frame pattern. In this pattern you split a program into three parts: The App, the Infrastructure and the Logic. The Logic is as close to pure functional as you can get. It's the business rules, algorithms, and calculations. The Infrastructure is responsible for reading input and writing output, and generally interacting with the outside world. The App is responsible for coordinating between the other two parts: Reading input using Infrastructure, processing it with the appropriate Logic, then routing it back to Infrastructure.
Why split things up like this? Splitting logic out from the other parts seems clear – it allows you to test that the logic works correctly. Logic can be nice and testable. But what about splitting App from Infrastructure? You're not gaining anything in extensibility by abstracting out the particular way we read command line arguments. So why do it?
Before answering this, I think it's worth considering the App vs. Infrastructure distinction. Consider the Traffic Cop code example. Why is processing a login form "application logic"? Why is 'returning' a redirect "application logic"? I think the answer is: Many applications may want to process a form, but every application has its own specific forms. This is true even for login forms – typically the specific logic of how logging in works is free to vary from application to application. This could be something you'd want to standardize into infrastructure in some circumstances – if you have several applications that should have identical login processes, for example. Similarly, many applications may want to redirect in some circumstances – that is why that function is an infrastructure one. But this specific call happens only under certain application-defined circumstances – which is why that call is part of the application layer. It is application-defined in the sense that for some apps, you may not want to return a redirect when login fails – for example, if you are writing a backend for a single-page app (SPA).
I think the point of this is that you can test the routing logic. Logic is supposed to be independent of the infrastructure. Infrastructure is supposed to contain only the logic of communicating with external systems. To route between them, you may need application-specific logic to handle things like printing an error when passed too many or too few arguments (as in his code example).
Now I'm wondering: What would this look like if you used a library like argparse to define a command-line interface (CLI)?
One idea would be to take advantage of the
parse_args() method which allows passing an arbitrary arguments list. This feels slightly against the spirit of Testing Without Mocks because it's using a third-party library directly. However, the main behavior is transparent (the behavior of
parse_args()), and I trust this API to remain stable enough. It could be deprecated, although I don't think there is much risk of that at the moment. So I think it is arguably fine to write code like:
from argparse import ArgumentParser import sys def main(argv): parser = ArgumentParser(...) parser.add_argument(...) ... args = parser.parse_args(argv) ... if __name__ == "__main__": main(sys.argv)
This seems fine.
Postscript: After some testing,
argparse probably does deserve a wrapper if you need to test it. There is at least one aspect that is not easily visible or changeable: The parser wants to print text, and you can't inject that dependency. That makes it tricky to test things like the "not enough arguments" case. How exactly to do that I'll leave for another time.