In Code Complete, when talking about his PPP pseudocode process, the author says you should write at the "level of intent". "(describing what the design does rather than how it will do it)"
This concept is applicable to view model tests as well, because of their special user-facing characteristics. They should be written as if they were written by the designer/UX person, describing user behavior scenarios as we might explain them to each other casually over coffee.
A tool that goes to extremes to allow developers to express test scenarios at the level of intent is the Cucumber testing DSL. Basically, the developer expresses the intent of the user in plain English and some rather crazy regular expressions are used behind the scenes to turn it into computer-actionable instructions.
The promise of Cucumber tests is that the designer/UX person (or other "stakeholder") can write the tests with you since you're just writing in English. I have found the promise is hard to realize, however, because of the onus it places on the stakeholder to collaborate directly on tedious scenario enumeration. However, getting the designer/UX person to sit down and discuss and tweak the scenarios after-the-fact is straightforward and extremely useful.
For the discussions to go smoothly, however, the scenarios need to be written in plain English, or in something extremely close to plain English that you can translate to plain English on the fly for the purposes of the discussion. The discussions do not have to be centered around an actual Cucumber specification, though, just a very well written spec that describes things at the 'level of intent'.
A widely followed developer, Chromatic, used to rail against the concept of domain specific languages, claiming that we should instead simply write software in the language of our domain. The distinction is subtle, but it applies here. Basically, we want to write our spec tests as if we are writing Cucumber tests without actually writing Cucumber tests (because they are a pain to write). One way of doing that is to rely heavily on well-named helper methods so that your individual tests are mostly composed of the helper methods. The name of each of the helper methods is written at the level of intent, so when you read the test, it is immediately obvious to you, and even obvious to the stakeholder, what is going on.
A very simple example of this would be:
it "should take a guest user to the guest home page after authentication" do
log_in_as(:guest_user)
should_be_on_page(:guest_home_page)
end
Also, to get good tests,
your templates have to be incredibly dumb (see logicless-template),
basically only being able to ask and act on yes/no questions.
For example, "should I show the profile photo?" or "should the 'next' button be enabled".
The reason is that then we can add a public method to the view model called ui_the_next_button_is_enabled
which can easily be tested.
Contrast that to a template which has embedded logic such as ng-show="data_done_loading && user_is_admin"
.
You've now placed logic in the template that is untestable at the unit level and must be tested using actual UI tests (like acceptance tests).
Given the pain of creating acceptance tests, you'll likely say "ah, it's not really worth testing!"
Maybe, maybe not.
That decision should be made uninfluenced by the difficulty of writing the test, though.
You shouldn't fall into the testing-concept-writing-tests-based-on-ease trap.
This was content that was in another page, but I'm putting in here in case it's helpful
The reason we write the test is to capture the requirement. The reason why tests are called specs is because they actually are supposed to be an executable specification the fact that the product people often don't hand us actual specifications, but rather hand us a... (I cut off here for some reason)
Unit tests and acceptance tests are documents first, and tests second. Their primary purpose is to formally document the design, structure, and behavior of the system. The fact that they automatically verify the design, structure, and behavior that they specify is wildly useful, but the specification is their true purpose.
The trouble with coding it this way is that you lose the specification. You lose the initial requirement. It's unclear why exactly the button should be showing.
"Higher-level APIs should be intent-oriented (think SLOs) rather than implementation-oriented (think control knobs)." (#)