Building Applications Using Castle RC2: Part I
Well, now that Castle RC2 is out, let us see what this release can do for us, shall we? After my recent talk, I got forums heavy on my mind, so let us build one.
You can get the release here. I suggest that you would get the MSI installer. After installing the MSI, open Visual Studio and create a new project, you should see this screen:
Choose Castle ActiveRecord Project and name the project "Castle.Forums.Model", name the solution (last text box) "Castle.Forums".
Note: Despite all my recent posts about Active Record + NHibernate 1.2 == Love (and Linq :-) ), the RC2 does not have these features, since they mostly rely on enhancements in NHibernate 1.2. Currently, we are focused on getting a stable release out of the door, and we don't want to add NHibernate 1.2 yet. Mostly because it is a moving target and that we want to have a stable release of Active Record before chasing the rainbow over the next hill.
Personally, I am using the trunk version for a lot of stuff, and I find it quite stable, but I
like to live dangerously. Also, I can see the need for a feature and add it throughout the stack, which is what I am mostly doing now, seeing what new & exciting places I can take Active Record + NHibernate 1.2. Overall, I think that both frameworks benefits from this.
Here is the solution that was created:
It created a test project for us! Yay! Let us start by looking at the AbstractModelTestCase class. It basically setting up everything that we need in order to start testing. The only thing that we need to do here is just to modify InitFramework() method so it would include our model assembly, and only initialize once. Like this:
protected virtual void InitFramework()
{
if(isInitialized )
return;
IConfigurationSource source = ActiveRecordSectionHandler.Instance;
ActiveRecordStarter.Initialize(System.Reflection.Assembly.Load("Castle.Forums.Model"), source);
isInitialized = true;
}
protected static bool isInitialized = false;
I also had to change the connection string in the app.config, to point to "test" instead of "mytestdatabase", which is the default. Note that you probably want to run the tests on a scratch database, since the default test behavior is to rebuild the schema at every test.
Now, let us build a list of tests that we want to have. I think that those are interesting;
- Can create user (name, email, hashed password)
- Can load user by name and password
- Can create a forum (name, manager)
- Can load list of forums
- Can create a message (title, content, author, forum, parent, root)
- Can load list of messages for forum.
- Can load hierarchical list of the last ten root (start) messages for a forum.
Again, a reminder, this is using NHibernate 1.0.2, so no generic collections.
Create a new file called UserTestCase and add the following:
[TestFixture]
public class UserTestCase : AbstractModelTestCase
{
}
Now, let us start writing the real test:
[Test]
public void Can_Create_And_Load_User_By_ID()
{
User user = new User("Ayende", "Ayende@example.com");
user.SetPassword("Scott/Tiger");
user.Create();
Flush();
CreateScope();//replace scope
User userFromDb = User.Find(user.Id);
Assert.IsFalse( ReferenceEquals(user, userFromDb), "got the same instnace even though we changed scopes!" );
Assert.AreEqual( "Ayende", userFromDb.Name , "User name did not match");
Assert.AreEqual("Ayende@example.com", userFromDb.Email, "Email did not match" );
Assert.IsTrue( user.PasswordMatch("Scott/Tiger"), "Password did not match" );
}
So, I had written the test, and now I know how I want my code to look. At the moment, it can't compile, because it has no User class. Add a User class to the Castle.Forums.Model assembly. I only want to get this test to work, so here is the implementation of the User class:
[ActiveRecord("Users")]
public class User : ActiveRecordBase<User>
{
int id;
string name;
string email;
[Field]
string hashedPassword;
public User()
{
}
public User(string name, string email)
{
this.name = name;
this.email = email;
}
[PrimaryKey]
public int Id
{
get { return id; }
set { id = value; }
}
[Property]
public string Name
{
get { return name; }
set { name = value; }
}
[Property]
public string Email
{
get { return email; }
set { email = value; }
}
public void SetPassword(string password)
{
hashedPassword = Hash(password);
}
public bool PasswordMatch(string maybePassword)
{
return hashedPassword == Hash(maybePassword);
}
public static string Hash(string value)
{
return value;
}
}
I don't have a test for the Hash() method yet, so I just had it return its input, now let us write a very simple test for it:
[Test]
public void Calling_User_Hash_Get_Diffrent_Return_Value()
{
string hashed = User.Hash("Scott/Tigger");
Assert.AreNotEqual("Scott/Tiger", hashed, "Password is the same even after hashing");
}
This one fails, obvious, so I add the following implementation:
public static string Hash(string value)
{
SHA512 hasher = SHA512.Create();
byte[] valueBytes = Encoding.UTF8.GetBytes(value);
byte[] hashedBytes = hasher.ComputeHash(valueBytes);
return Convert.ToBase64String(hashedBytes);
}
Now we need to load a user by name and password, let us write the test first:
[Test]
public void Can_Load_User_By_Name_And_Password()
{
User user = new User("Ayende", "Ayende@example.com");
user.SetPassword("Scott/Tiger");
user.Create();
Flush();
CreateScope();
User userFromDb = User.FindByNameAndPassword("Ayende", "Scott/Tiger");
Assert.IsNotNull(userFromDb, "Could not find user by username and password");
}
Implementing FindByNameAndPassword...
public static User FindByNameAndPassword(string name, string password)
{
string hashedPassword = Hash(password);
return FindOne(Expression.Eq("Name", name),
Expression.Eq("hashedPassword", hashedPassword));
}
Looks like we are through with test for users, in a similar fashion, we are going to build the forums.
In ForumTestCase:
public override void Init()
{
base.Init();
this.user = new User("Ayende", "Ayende@example.org");
this.user.SetPassword("Scott/Tiger");
this.user.Create();
}
[Test]
public void Can_Create_And_Load_Forum()
{
Forum forum = new Forum("My Forum", user);
forum.Create();
Flush();
CreateScope();
Forum forumFromDb = Forums.Find(forum.Id);
Assert.AreEqual("My Forum", forumFromDb.Name, "Forum name not the same");
//note: can't do a simple comparision because the arrive from different scopes.
Assert.AreEqual(user.Id, forum.Manager.Id, "Forum manager is not the same" );
}
And the Forum class implementation:
[ActiveRecord]
public class Forum : ActiveRecordBase<Forum>
{
int id;
string name;
User manager;
public Forum()
{
}
public Forum(string name, User manager)
{
this.name = name;
this.manager = manager;
}
[PrimaryKey]
public int Id
{
get { return id; }
set { id = value; }
}
[Property]
public string Name
{
get { return name; }
set { name = value; }
}
[BelongsTo]
public User Manager
{
get { return manager; }
set { manager = value; }
}
}
Along the way, we also go the second test for free:
[Test]
public void Can_Load_List_Of_Forums()
{
Forum forum = new Forum("My Forum", user);
forum.Create();
Forum forum2 = new Forum("My Forum 2", user);
forum2.Create();
Forum[] allForums = Forum.FindAll();
Assert.AreEqual(2, allForums.Length);
}
That is all for forums, now to messages. First, the test:
[Test]
public void Can_Create_And_Load_Message()
{
Message msg = new Message("Hi there", "Hello World", user, forum, null);
msg.Create();
Flush();
CreateScope();
Message msgFromDb = Message.Find(msg.Id);
Assert.AreEqual("Hi there", msgFromDb.