Wednesday, January 13, 2010

Spring 3 and AspectJ

Moving to Spring 3 (and dropping the uber jar), my dependencies look a bit different. Eclipse started refusing to run tests: org/aspectj/lang/Signature not found, but only when running JUnit tests in Eclipse - Maven builds were fine.

After much searching, poking and prodding, I finally took the time to read the first line in the docs (http://maven.apache.org/plugins/maven-eclipse-plugin/examples/ajdt-projects.html): "The Eclipse plugin interacts with the Aspectj plugin to configure Eclipse to use AJDT." I had assumed, looking at their sample, it was present to configure specific things in AspectJ, and omitted the entire definition since I had no use for those configurations.

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
</plugin>

Adding this simple plugin to my made my classpath worries disappear. Woot, back to Spring 3 REST!

Monday, August 31, 2009

Maven EAR plugin and JBoss configuration

I had added a <jboss><version>4.2</version></jboss> configuration item to my project's EAR plugin configuration. It started deploying the contained WAR as OnlyASurvey-1.0-SNAPSHOT.ear. This confused JBoss quite a bit as it was expecting OnlyASurvey.war. Removing the configuration reverted the filename.

Recorded here for my future reference and for anyone else who runs across this problem.

Friday, August 28, 2009

Optimized Workstations, Part 1

Today I optimized my local PostgreSQL install a bit. Specifically I edited postgresql.conf and adjusted parameters that I thought were relevant. I used the list of configuration items in the General Tuning section of this post and common sense as a guide. I turned off the fsync option, figuring that syncs are pretty useless in a begin/rollback transactional test architecture.

My test suite went from 45-50 seconds to a reliable 37-38 seconds for each run.

For someone who runs tests so often this ~10 second gain gets me a lot - 20% faster test suite execution. For a floor of 40 developers costing the shop ~$1/second this can add up.

Let's take a smallish project with a robust 2 minute test suite run. Shaving 20%, or 24 seconds, off the run and assuming we run the whole suite 5 times per day takes our (120*5 = $600/day) test runs to (96*5 = $480/day), a savings of $120/day or $600/week or $31,200/52-week year.

Not bad for 10 minutes of investigation and a service restart.

Monday, August 24, 2009

BDAS 101

One of my top priorities when taking my first technical lead position was establishing a change advisory board to help me steer a shop of 30-50 developers (dependent on load). While other CABs existed in the organization, they were corporate and this was just for developers, so I didn't label it as a CAB.

Instead I put together the Build, Design and Architectural Standards group, a.k.a. BDAS, which quickly became known simply as "badass". I would have included Process, which is also in scope, but then the acronym would have been BDAPS, and I couldn't have that.

Over time I will add more posts about my experiences in BDAS. Today I'll offer some advice on running a core developer meeting.


Developers can't have a frank discussion with a cop in the room

It was important for the group to be made entirely of developers. There is nothing sadder to watch than a competent engineer attempting to discover what the boss wants to hear. I needed a group that could call it as they saw it and felt they were in a room where it was okay to do so.


Have the right people

Team leads, senior developers, movers and shakers. The group is going to define standards, make recommendations and guidelines and, in the case of some, manage their own teams in implementation. It's important to have representation from all stakeholders who are involved in the actual building and maintaining of source code. That means all development teams, pretty much any group that will be committing to your SCR.


Process, process, process

If you're going to take ten people's time, an hour every week, to get them into a room and try to get opinions and decisions out of them, first bring timbits. Then make sure that everyone knows what the gig is about, their role in it and how it's going to go down. Have a standard agenda format, track items that can be closed, produce minutes in a central place and do all of it yourself. Link all meetings, resulting tickets, wiki pages, documents, code.

Today BDAS consists of a landing page, links to meetings as wiki pages simply titled "BDAS Meeting 1..n" and a JIRA project to track items. Each week, preferably well in advance, start a wiki page with the next sequential meeting number. Add a link to the previous meeting, list agenda items as links to your issue tracker, and add a Minutes header. Then as the meeting goes on update the page to fill in the minutes. A BDAS meeting page looks roughly like the following:

BDAS Meeting 20
  1. Review minutes of BDAS Meeting 19
  2. Agenda
    • BDAS-1: Standardizing on autowiring and component scanning
    • BDAS-3: Establish acceptable mockup formats
    • BDAS-29: Review domain model support API
    • Open discussion
  3. Minutes
    • whatever comes up, link to proposal doc we created, etc

From this page link to anything else that's relevant. If you control that content then link back to it. Link to issues, source code (e.g., FishEye, SVN), vendors, whatever. A hugely important aspect of BDAS is transparency and discoverability.


If you're done, close the meeting

Nothing you do as a tech lead will get you more brownie points than ending a meeting early. If you've worked the agenda, done a round table and have nothing left in the plan then you're done for the day.


If you're not done, leave it for next meeting

Never run over. More specifically, don't be the cause of it. If you just can't stop others from making the meeting go longer than scheduled then make sure you keep trying to end it, suggest offline discussion, offer to put it on the table for next week or schedule a meeting of interested parties if it's so important, but get bodies out of the room.


Friday, July 24, 2009

Domain Model Testing

To me, measuring coverage is not about the coverage measured (regression, certainty), it's about what's left uncovered (risk, agility) and why I should care (cost, agility). I'd prefer to have 100% coverage and be confident that it's due to ninja coders writing ninja code. In RL I only ask that model coverage be in line with the project's overall coverage, starting with the most complex methods. Maximize coverage and minimize cost, a.k.a. the simplest (smallest) code that could possibly work.

There is always debate over testing getters and setters, or even measuring their coverage at all or including it in reports. It's seen as unfair to measure and derogatory towards one's stats. I think that misses the point.

So here's what I do and why.

There's a lot to a domain model. Herein I'm referring to persistent models, i.e., Hibernate-mapped classes (or similar, depending on the platform). If you've worked with Hibernate applications in the real world then you have likely seen it load the entire database, cascade deletes you didn't expect, and throw the occasional exception complaining about possibly unsafe thread access. These have nothing to do with getters and setters, though in exercising your model you will eventually exercise nearly all such methods.

So what are some common issues that persistent models face? Some that I've seen include:
  • Forgetting metadata (@Enumerated !!)
  • Omitting constraints - either on the model or -- more importantly -- in the database
  • Inappropriate eagerness and laziness
  • Unnecessary relationships, particularly bidirectional ones
  • Missing or buggy equals and hashCode methods
  • Impact of broken transactional schemes, particularly as you make changes later
Some of the waste that I see when reviewing code involves creating test data specific to a test. Not only is this redundant RY-ism, it increases the cost of changing a model markedly, and obfuscates the wider impact of those changes.

Instead, over time I have come to use a pattern that I find very useful and inexpensive.
  1. Create a test scenario data service
  2. Maintain a domain model test
  3. Pervasively use scenarios in tests

Test Scenario Data Service

Except for very simple instances I always create data using a test data service. Something like the following:

@Service
@Transactional
public class TestDataServiceImpl implements TestDataService
{
/** {@inheritDoc} Thorough, accurate. */
public Survey createScenario1(Account owner) {
return createScenario1(owner, true);
}

/** {@inheritDoc} Thorough, accurate. */
public Survey createScenario1(Account owner, boolean persist) {
Assert.notNull(owner);
Survey survey = new Survey();
survey.setOwner(owner);
survey.setName("My Test Survey #" + getMBUN());
if(persist) {
persist(retval);
}

return survey;
}

/** {@inheritDoc} Thorough, accurate. */
public Survey createScenario2(Account owner, boolean persist) {
Assert.notNull(owner);
Survey retval = createScenario1(owner, false);
retval.addQuestion(new TextQuestion(retval, "Please enter your email address:"));
if(persist) {
persist(retval);
}

return survey;
}
}

This class is then wired into pretty close to 100% of my test classes. As you can see, the createScenarioX methods often have a variant with a "boolean persist" flag, true by default, so that scenarios can build on each other without prematurely persisting information and for those times when a unit test is in order -- consistency in the scenario model maintains velocity in testing.


Maintain a Domain Model Test

In each of my projects can typically be found a class called DomainModelTest. It contains at least one test for each scenario in my TestDataService. These tests will invoke the createScenarioX() method, flush and clear the Hibernate session, then reload the root object and walk it, comparing values and references along the way. While hard coded, not only do these tests exercise accessor methods (metrics for management) they also:
  1. document the domain model
  2. document variants, use cases and negative tests
  3. alert you to damage done to the test database e.g., by corporate DBAs or by inappropriate or incompetent resources, typically after you're off the project
  4. fail (often fantastically) due to performance issues when the suite runs against a copy of production - therefore, run it against a copy of production from time to time.

Pervasively Use Scenarios in Tests

Any test that I write that involves data being loaded, changed, deleted, queried or otherwise interacting with a model will always use the test service and usually in a persistent way. No mocks here, thanks.

The trick (and goal) is to use the scenarios you created earlier pervasively throughout your entire test suite. Not only does this reduce the cost of scenario creation, it reduces the cognitive load of inspecting, changing and maintaining your test suite because all references to createScenario3() do the same thing, cost only 1 line of code, and set the expectation for the context of the rest of the test in question. This in turn enables estimating the impact of changes to your model in a more deterministic manner.


Lessons Learned

The big lesson that I've learned applying this pattern is that at first you want to create the most complete scenario possible -- resist and heed my warning because each time you change or extend the model, Scenario #1 gets updated and so does the corresponding domain model test and most other tests in the suite. All green, all good, at first. What happens is that after a while (say, 500 tests later) you have a test suite that takes on the order of 45 seconds to run. Come on, I don't have all minute!

Here are some recommendations:
  • Don't be afraid of multiple scenarios - basicScenario1, complexScenario2, basicScenario3WithBranchingLogic, etc. Start small and expand, and consider having a "maximum scenario" which implements every possible model component. Use this latter scenario in the domain model test often, and when wanting to generate a largish dataset for performance testing
  • Start small - Scenario #1 should be the minimal dataset required to be persisted without errors
  • boolean persist - being able to construct a scenario without persisting it means you can customize a particular scenario or write a unit test. This is not always possible, particularly if you have any logic in the backend - document that clearly in the javadoc
  • Have a @Test createReallyBigDataSetSanityTest() and make sure you flush(). It should create n scenarios, of differing types, where n is something that fits in a reasonable timeout. You will catch missing indexes, Hibernate loading too much data and various other sundry issues
  • Iterating a test implementation against multiple scenarios is a powerful technique; consider having a List<Type> getAllScenarios() method or multiple ones if your various scenarios create trees with different roots. It really sucks to have test failures dependent on backend data because users will inevitably create all possible scenarios in production



Tuesday, June 30, 2009

Bash shell configuration

Further to my last post about bash, it really bugs me having to `exec bash` because my .bashrc isn't being executed when I login. After changing my shell to /bin/bash I was able to get a useful command prompt thanks to the various defaults in place and moved on. But when I run dir (no, I can't just ls -l nor ll like normal people) I get the same horizontal ls lameness, not the dir='ls -l --color' goodness that I expect.

I found this thread which showed the solution for my Ubuntu box. So I put

source ~/.bashrc

into my .bash_profile and now when I su to the account I get my environment exactly as I configured it.

Factoid: It turns out (man pages ftw) that bash's default is to ignore any.bashrc file if it is being executed as "sh", so even if /bin/sh is symlinked to /bin/bash the script won't be run as I assumed it would.

Monday, June 29, 2009

Missing Bash Prompt

When working on new machines it always frustrates me that I end up working at a simple "$" prompt, nothing else, because .bashrc is not being used. If this happens to you, you are likely using /bin/sh and not /bin/bash on a machine where the former is not symlinked to the latter (which you may be expecting).

$ chsh

Will allow you to change your shell.