It took me quite a while to get into writing Nant-scripts for my projects. One of the main reasons for me to denounce the concept of writing buildfiles at all was that I was all too happy to be rid of those terrible makefiles I had to put up with before we had Visual Studio. Why think about what files to include, what parameters (in what order) to use during compilation and where to copy those files to get something to work when you can just press F7 (yeah I still have VC++6 keybinds) and wait for your harddisk to stop rattling? Well, as it turns out, it's actually quite a good sanity check on your build environment, plus it helps you think about installer-related problems that will pop up eventually anyway.
The way to go about this is to write a Nant script that will set up your project on a machine that hasn't previously been used to work on the project. It doesn't have to install Visual Studio, the SDK or any other required tools. We use Subversion for sourcecontrol and the general way to pull out a project is to get out the trunk of the project non-recursively (the -N parameter does this when using the svn.exe commandline tool). What you get is a readme.txt which tells you which versions of the buildtools you need, a setup.include file which contains all configurable properties and finally a setup.build, the actual buildfile. All I have to do now is type nant and sit back, while the entire project is set up and ready to run or work on right on my machine.
What happened when I started writing a script like that is that I noticed how some of the quick decisions that were made during the startup of the project were not exactly the right way to do things. For instance, the log4net install was in a subdirectory of the main project-directory. Doesn't make sense ofcourse because other projects may want to use this library as well, so I moved it around.
The database runs on the same machine for development as IIS, so the local ASPNET-user was added as a login to SqlServer and that was that. Try getting that to work without having rights to creating domain users in a scenario where IIS and SqlServer run on seperate machines. I had to jump through several hoops changing values in either machine.config and web.config, encrypting values in the registry and creating mirror accounts on both machines. This would pop up eventually anyway, but running into it before the customer is waiting for everything to go online within fifteen minutes ensures you can solve this elegantly (as far as that's possible anyway, but that's another story).
As for getting ahead on troubles you'll run into when creating an installer: some of the views were renamed in SqlServer during development. It turns out that if you do this, SqlServer messes up the dependencies so the script that is generated probably won't run on an empty database. So you remove those views and recreate them instead. It's better to tackle these kind of things daily than run into them all at once when your projects is supposed to be nearing completion.
And best of all, working with these scripts gives me the same kind of secure feeling about the state of the project as using unit-tests while refactoring. In fact, every monday morning the first thing I do is remove my project-directory (I used to rename it to .old first, just to be sure), check out the buildfile and recreate the build-environment. This way I know for sure there's nothing vital to the project on my machine that's not in the repository.
Frans is annoyed with all the SOA marketing going around. Although I fully agree with his point that technically it's nothing new and just the continuation of architectural principles established decades ago, I do think that it's worth considering how they are best implemented in the current environment. The current environment consisting of fat clients everywhere all hooked up with some kind of broadband connection to the Internet and tuned to work best with *ML-formatted data going across a TCP/IP connection using port 80.
So when you conclude this landscape will remain so for the coming years or will in fact be more and more appropriate for this technology (which is to be expected since I don't see the web going anywhere soon) then it's interesting to think about how these basic client/server-principles can most effectively be mapped to the applications you want to develop. SOA is apparantly Microsoft's plan for this.
Thanks to NUnit, unit-testing in a .NET-environment is really easy. With good tools however, it's still fairly easy to make stupid mistakes. One of the things I've learned when constructing unit-tests is that you shouldn't reuse testing code to create the actual code. Ofcourse that's obvious, but in some cases, it's quite easy to do it all wrong and end up in the debugger anyway.
For instance, let's consider a class Loan (I was going to suggest Mortgage but was afraid that mentioning that word in any kind of internet-based communication would put me on all possible spam-blocklists) that has several properties. You set the YearlyInterestRate or MonthlyInterestRate, which are both read/write-properties and as such need to calculate eachother when you set one of them. So you write an NUnit-test to see whether the calculation is correct:
[Test]
public void YearlyToMonthlyInterestRate()
{
Loan loan = new Loan();
double yearlyrate = 8.7d;
loan.YearlyInterestRate = yearlyrate;
Assert.AreEqual((Math.Pow(1+(yearlyrate/100), 1/12)-1)*100,
loan.MonthlyInterestRate);
}
You construct the object, set the YearlyInterestRate, then calculate the monthly rate and compare that to the value in the corresponding property. Compiling fails, so you construct a dummy class, then you run it, the test fails and you're back to write the actual class:
using System;
public class Loan
{
private double yearlyrate;
private double monthlyrate;
public double YearlyInterestRate
{
set
{
yearlyrate = value;
monthlyrate = (Math.Pow((1+(yearlyrate/100)), 1/12)-1)*100;
}
}
public double MonthlyInterestRate
{
get
{
return monthlyrate;
}
}
}
Compiling works and running comes up with a green test! This means the code works right? Unfortunately, it doesn't. We forgot to cast the part where we do 1/12 to double so we end up raising to 0. In both the test and the actual class. 0 is equal to 0, so the test succeeds and the functionality appears to work. Until it's used in production code and all your client's customers who opted to pay their interest monthly never have to pay anything.
Even if you didn't copy/paste the calculation code, if that's the way you remember to do that calculation, there's a chance you'll write the same bug twice anyway. The only way around this is to write multiple tests: one to test whether the correct algorithm is used (at least, if you care about that in the requirements) and another to test whether the results are at least plausible. For instance, we could have added a test to check whether if the yearly rate is non-zero the monthly is as well, or even if both rates are not the same (if they're non-zero, that is).
What I do for this kind of stuff is just use my calculator to come up with some numbers. If the yearly rate is 8.7%, the monthly rate is around 0.7%. NUnit's Assert.AreEqual-method has a great overload that allows you to provide a delta:
[Test]
public void YearlyToMonthlyInterestRate2()
{
Loan loan = new Loan();
loan.YearlyInterestRate = 8.7d;
Assert.AreEqual(0.7d, loan.MonthlyInterestRate, 0.01d);
}
This test would have caught the bug. So the answer to a question people ask me a lot when it comes to test-driven development: "But how do you know the tests are correct?" is that you don't, but by making multiple tests to cover the same functionality you're getting very close.
That's right, one of my resolutions is to write to my blog again, so here I am on the first day of 2005 doing exactly that. It's been quite a hectic past four months with my .NET-project actually getting finished and released. But before we get into that: Happy new year everyone!