Oren Eini

CEO of RavenDB

a NoSQL Open Source Document Database

Get in touch with me:

oren@ravendb.net +972 52-548-6969

Posts: 7,546
|
Comments: 51,163
Privacy Policy · Terms
filter by tags archive
time to read 124 min | 24681 words

Last time I left we had a working data access layer, based on Active Record, as well as a scaffolding for editing users. We used TDD to drive the functionality of the application, and I intend to keep doing it this way while developing the functionality of the site itself.

Before we start by building the initial list of tests, let us consider what we want to achieve...

We want to have a forum system, so diplaying the forums, adding and browsing messages is very important, but managing users / forums is not a high priority, we can leave the users management on the scaffolding, and build the scaffolding for forums as well. Here is what we need to do to build the scaffolding for Forum:

[Layout("default"), Rescue("generalerror"), Scaffolding(typeof(Forum))]

public class ForumController : SmartDispatcherController

{

}

And now we can access it at /forum/list.rails

One thing that I forgot to do before is to add the ActiveRecord HttpModule, which goes into the <httpModules> section in <system.web> element, like this:

<add name="active-record"

        type="Castle.ActiveRecord.Framework.SessionScopeWebModule, Castle.ActiveRecord" />

Now, let us see what kind of scenarios we want to support:

  • The homepage should display a list of all the forums, by name and id, with links to the forum page.
  • There should be a text box where I can enter a forum id and go directly to it.
  • A forum should display ten root messages, and be pagable.
  • Can add a message to forum.
  • Can reply to message.
  • Messages appear in hierarchy.

When building tests for a MonoRail application, we have two options, we can do normal tests, which involve testing the data sent from the controller to the view, or integration tests, where we assert on the HTML output from the view. Let us start with the first test, asserting that the homepage controller pass the view a list of all forums. We start by building the test data in the test setup. But in order to do this, we need to setup Active Record.

In the previous installment, we already setup Active Record for the MonoRail application, but now we are calling Active Record from the tests, and that is in a different AppDomain.

A word about the way MonoRail tests work.
A MonoRail test basically host the ASP.Net runtime in a different AppDomain, which means that anything that goes between your test and the application is cross AppDomain boundaries.
This is why we need to setup Active Record in both the tests and the MonoRail application, they are on different app domains.
One word of caution, right now we have setup the MonoRail application so that it would recreate the database on startup, but we setup the database on test setup. This means that we need to remove the ActiveRecordStart.CreateSchema() from Application_OnStart.

I decided to do the setup in a test base class, like this:

public class AbstractMRWithARTestCase : AbstractMRTestCase

{

       [SetUp]

       public override void Initialize()

       {

              ActiveRecordStarter.ResetInitializationFlag();

              IConfigurationSource source = ActiveRecordSectionHandler.Instance;

              ActiveRecordStarter.Initialize(System.Reflection.Assembly.Load("Castle.Forums.Model"), source);

              ActiveRecordStarter.CreateSchema();

              base.Initialize();

       }

}

This initialize the framework, and then rebuild the database, so the tests are isolated.

Now, let us see the real setup code:

[SetUp]
public override void Initialize()
{
       base.Initialize();

       User user = new User("Ayende", "Ayende@example.org");

       user.SetPassword("Scott/Tiger");

       user.Create();

 

       Forum forum1 = new Forum("My Forum 1", user);

       forum1.Create();

 

       Forum forum2 = new Forum("My Forum 2", user);

       forum2.Create();

}

Now, after all this, we can start creating the first real test, it goes like this:

[Test]

public void IndexAction_PassAllForumsToView()

{

       DoGet("home/index.rails");

 

       AssertPropertyBagContains("forums");

       Forum[] forums = (Forum[])Response.PropertyBag["forums"];

       Assert.AreEqual(2, forums.Length);

 

}

What does this do? Well, it asserts that the controller has passed two forums to the view. Running this test now will fail with this erro:

Entry forums was not on PropertyBag

We have a red light! Let us make it green. We can make this test pass using this code:

public void Index()

{

       PropertyBag["forums"] = Forum.FindAll();

}

We run the code, and this time it is failing with another error, complaining that Forum is not a serializable type. This is an annoying side affect of the need to run ASP.Net in another AppDomain, everything that goes between the domains has to be serializable. This is an annoying limitation, and we are thinking about ways to fix this, but currently there isn't a good way to do this, since the ASP.Net runtime creates its own AppDomain, and that is hard to change. So, we need to put [Serializable] on Forum, User and Message classes. I usually don't like artificial test requirements, but this isn't an onerous one, and there are valid reasons to want to do this anyway.

After making the model entities serializable, the test pass! Of course, if we go to the page, it doesn't show anything, we need another test, one that would assert that the view is displaying the fourms:

[Test]

public void IndexView_DisplayAllForums()

{

       DoGet("home/index.rails");

      

       AssertReplyContains("My Forum 1");

       AssertReplyContains("My Forum 2");

}

Run and see it fails... Let us make this work. Go to the view, which is on the Castle.Forums.Web project, "Views\Home\index.boo". We need to output the forums out, so let us do it simple as:

<h2>Forums List</h2>

<ul>

      <?brail for forum in forums: ?>

      <li>${forum.Id} - ${forum.Name}</li>

      <?brail end ?>

</ul>

Run the test again to make sure that it passes, and let us move on. We don't have a list to each forum's page, but this is because it doesn't exists yet. So let us create it. I think that /forum/1/index.rails is a good place to put it, so let us create a ForumControllerTestCase class, for testing the new functionality. In the setup (which I will not show) we merely create a user, a forum and 50 messages (each with 2 reply messages). The root messages has names like Root 1 and Root 30, and their children names like Child 1.3 or Child 30.2, this is just so we can assert on them with ease.

Let us start by writing the test:

[Test]

public void ForumController_PassAllRootMessagesToView()

{

       DoGet(string.Format("forum/{0}/index.rails", forumId));

       AssertSuccess();

      

       AssertPropertyBagContains("messages");

       Message[] msgs = (Message[])Response.PropertyBag["messages"];

       Assert.AreEqual(50,  msgs.Length);

}

Very similar to what we did before. Notice that forumId is the id of the forum we create previously. Now, to get the to pass we need to do quite a bit.

First, let us add an index method to ForumController:

public void Index(int forumId)

{

       Forum forum = Forum.Find(forumId);

       PropertyBag["messages"] = Message.FindAll(Expression.Eq("Forum", forum),

              Expression.IsNull("RootParent"));

}

Get the forum, and then get all the message for the forum, very simple. Note that this method is called by the MonoRail framework, and it has a parameter. This parameter is parsed by MonoRail from the request parameters (both query string and forms parameters are considered).

Considerring that the URL that we want to use has the format of "/forum/1/index.rails", how can MonoRail understand that it should use the 1 as the parameter to the index? We can tell it so with the use of routing rules. A routing rule is merely a small piece of regex that transfrom one representation of the request URL to another.

You can configure routing using the <monorail>/<routing> element, by adding a rule:

<rule>

       <pattern>/forum/(\d+)/(.*)$</pattern>

       <replace><![CDATA[ /forum/$2?forumId=$1 ]]></replace>

</rule>

We are simple replacing every reference to /forum/number/page with /forum/page?id=number. Now, MonoRail can translate the "folder" into the correct parameter. We are not done yet, we still need to create a view to show a forum's listing. We need to create a a subdirectory under views "Forum", which will contain all the forum's views. Then, add an empty file called index.boo to it.

Now the test passes, but we have nothing in the view. Also, notice that we aren't doing any paging yet.

Here is a test that assert that the page shows something, and that it is using paging:

[Test]

public void ForumsView_ShowsOnlyFirstRootMessages()

{

       DoGet(string.Format("forum/{0}/index.rails", forumId));

      

       AssertReplyContains("Root 1");

       AssertReplyContains("Root 9");

 

       AssertReplyDoesNotContain("Root 11");//that should be paged

}

MonoRail has a feature called CachedPagination, which we are about to make use of. It means that we are going to need to change the controller code, and since the pagination is not serializable, so we need to restructure our tests first. Let us start with the code this time:

public void Index(int forumId)

{

       string paginationCacheKey = string.Format("forum.{0}.messages", forumId);

       DataObtentionDelegate getMessageForForum = delegate

       {

              Forum forum = Forum.Find(forumId);

              return Message.FindAll(Expression.Eq("Forum", forum), Expression.IsNull("RootParent"));

       };

       IPaginatedPage cachedPagination = PaginationHelper.CreateCachedPagination(paginationCacheKey,10
                                                        getMessageForForum);

       PropertyBag["messages"] =  cachedPagination;

       Flash["messages.TotalItems"] = cachedPagination.TotalItems;

}

What is going on? We create a cache key for the results, this should help reduce the amount of queries that we have in a big site, not really neccecary in our case, but it is so elegant that I can't help it ;-) We pass a delegate to the method, which will be called when there is a cache miss to get the data. Then we put the resulting pagination and the total item count into it. Notice that I don't need to specify which page I was working with, that is the responsability of the PaginationHelper.

I put the total count into the Flash so I could still test that I am getting the results from the controller. Since pagination is not serializble, I can no longer access PropertyBag, it can't pass the AppDomain boundary. The first test now looks like this:

[Test]

public void ForumController_PassAllRootMessagesToView()

{

       DoGet(string.Format("forum/{0}/index.rails", forumId));

       AssertSuccess();

 

       AssertFlashContains("messages.TotalItems");

       int msgs = (int)Response.Flash["messages.TotalItems"];

       Assert.AreEqual(50,  msgs);

}

Moving on to writing the view itself, it looks like this:

<h2>Messages:</h2>

<table>

      <tr>

            <td>Title:</td>

            <td>Author:</td>

      </tr>

      <?brail for msg in messages: ?>

      <tr>

            <td>

                  ${msg.Title}

            </td>

            <td>

                  ${msg.Author.Name}

            </td>

      </tr>

      <?brail end ?>

</table>

Now, when we run the tests, everything pass. But we need to hanle paging as well. Currently we are just showing the first 10 rows. Here is a test that says we can page:

[Test]

public void ForumsView_HasPaging_AndChangeDataOnPaging()

{

       DoGet(string.Format("forum/{0}/index.rails", forumId));

      

       //has next link

       AssertReplyContains("<a href=\"/forum/index.rails?page=2&amp;forumId=1&amp;\" >next</a>");

 

       DoGet(string.Format("forum/{0}/index.rails", forumId), "page=3");

      

       AssertReplyContains("Root 20");

       AssertReplyContains("Root 29");

 

       AssertReplyDoesNotContain("Root 19");

       AssertReplyDoesNotContain("Root 31");

}

This is fairly brute force way of doing this, but I can't really this of a better way. Also, notice the way of passing query strings using DoGet(), if you try to concat strings to make a URL, it won't work. Anyway, we need paging, we are likely going to want to do this in more than one view since paging is a cross cutting concerns. We are going to utilize the CommonScripts functionality of Brail to get this. Add a subfolder called CommonScripts to the Views folder, and create a file called Paging.boo:

import Castle.MonoRail.Framework.Helpers

import System.Text

import System.Collections

 

def Pagination(paginationHelper as PaginationHelper,

      items as IPaginatedPage,

      htmlAttributes as IDictionary,

      querySting as IDictionary):

      output = StringBuilder()

      output.Append("""<div class="pagination">

            <table width="100%"

                     border="0">

                  <tr>

                        <td>Showing ${items.FirstItem} - ${items.LastItem} of ${items.TotalItems}</td>

                        <td align="right">""")

      if items.HasFirst:

            output.Append( paginationHelper.CreatePageLink( 1, "first",htmlAttributes, querySting )  )

      else:

            output.Append( "first" )

      end

      output.Append( " | " )

      if items.HasPrevious:

            output.Append( paginationHelper.CreatePageLink( items.PreviousIndex, "prev",htmlAttributes, querySting )  )

      else:

            output.Append( "prev" )

      end

      output.Append( " | " )

      if items.HasNext:

            output.Append( paginationHelper.CreatePageLink( items.NextIndex, "next",htmlAttributes, querySting ) )

      else:

            output.Append( "next" )

      end

      output.Append(  " | " )

      if items.HasLast:

            output.Append(  paginationHelper.CreatePageLink( items.LastIndex, "last",htmlAttributes, querySting ) )

      else:

            output.Append(  "last" )

      end

      output.Append( """</td>

            </tr>

      </table>

</div>""")

      return output.ToString()

end

This looks like a lot of code, but it isn't doing much, actually. Just outputing the pagination html. Notice that we can specify the query string and the html attributes. Now, we can just append this at the bottom of the views:

<?brail

output Pagination( PaginationHelper, messages, null, { "forumId" : forumId} )

?>

Now run the tests, assure that they are passing, and then browse to the site and see what your easy work has gained you :-)

I'm not going to write more tests, by this time, you should be able to do this yourself, and they clutter the tutorial.
By this point, I think that I have shown how easy it is to write tests for MonoRail.

Now that we have a way of showing the forum's messages, we can add a link from the home page to the forum's page, and then we are done with the first scenario, this can be done as simply as:

<li>${forum.Id} - <a href="/forum/${forum.Id}/index.rails">${forum.Name}</a>

Now we finished with the first scenario, the second one calls for a text box that allows a quick access to the forum's page by its id. It turns out that we have to change only the view to make this happen, just adding this to /home/index.boo:

<div style="text-align: right;">

      <form action="/forum/index.rails">

            Go to forum #

            <input type="text" name="forumId"/>

            <input type="submit" value="Go!"/>

      </form>

</div>

How this works? Remember that the ForumController.Index() method accept an integer? This simply sends it to it. MonoRail doesn't care if how we pass the value, it does the Right Thing :-D

But, this raise the question of input validation. We now have input from the user, and they are very good in putting the wrong data just to spite me.

Looking at this, I can see three edge-cases:

  • Entering an id that doesn't belong to a forum.
  • Not entering anything.
  • Entering a non-numeric value.

Let us protect ourself from the first case:

public void Index(int forumId)

{

       Forum forum = Forum.TryFind(forumId);

       if(forum==null)

       {

              PropertyBag["ForumId"] = forumId;

              RenderView("NoSuchForum");

              return;

       }

       string paginationCacheKey = string.Format("forum.{0}.messages", forumId);

       DataObtentionDelegate getMessageForForum = delegate

       {

              return Message.FindAll(Expression.Eq("Forum", forum), Expression.IsNull("RootParent"));

       };

       IPaginatedPage cachedPagination = PaginationHelper.CreateCachedPagination(paginationCacheKey,10,

                                                      getMessageForForum);

       PropertyBag["messages"] =  cachedPagination;

       Flash["messages.TotalItems"] = cachedPagination.TotalItems;

}

We simply add a check to ensure that the forum exists. In both cases, we show a view that simply says: (Views\Forum\NoSuchForum.boo)

<h2>No Such Forum</h2>

<p>

Could not find a forum with id ${ForumId}

</p>

This still doesn't handle the last case, where the user pass a non-numeric data. How would we handle this? We can try to catch an error from the framework when it is trying to convert the value to integer, and handle that, but that is not an elegant solution in my opinion. A much more elegant solution is to think about it this way, an invalid integer value is a string, and MonoRail actually supports overloading, so... here is how we handle the other two cases:

public void Index(string forumId)

{

       if (string.IsNullOrEmpty(forumId))

              PropertyBag["ForumId"] = "null / empty";

       else

              PropertyBag["ForumId"] = forumId;

       RenderView("NoSuchForum");

}

Now MonoRail will do the right thing, and call our Index(int) if the value can be converted to integer, and call Index(string) if it can't. We moved our error handling outside the method, not it is much easier to read and work with. Also, we are reusing the UI, the NoSuchForum view is being used by two actions on the controller. That is something that is usually harder to do (in terms of the setup require to make the code reusable), isn't it?

A word of caution, this behavior is currently dependant on source code orderring, which I consider a bug. This mean that the Index(int) method should come before Index(string) method for it to be considered.

Forth scenario, we need to be able to add messages to a forum. Let us extend the forum/index view to handle this. I think that I want to use a drop of ajax for this, so we need to add this to Views\Layouts\default.boo, somewhere on the <head>:

${AjaxHelper.GetJavascriptFunctions()}

This makes sure that prototype is setup appropriately. Now, we can just append this at the end:

<p>

      ${ AjaxHelper.LinkToFunction('Post new message...', '$(newMsg).toggle()') }

</p>

<div id="newMsg"  class="postNewMsg" style="display: none;">

      <form method="POST">

            <table>

                  <tr>

                        <td>Title:</td>

                        <td><input name="msg.Title" type="text"/>

                        </td>

                  </tr>

                  <tr>

                        <td>Author Id:</td>

                        <td><input name="msg.Author.Id" type="text"/>

                        </td>

                  </tr>

                  <tr>

                        <td>Content:</td>

                        <td></td>

                  </tr>

                  <tr>

                        <td colspan="2">

                              <textarea name="msg.Content" rows="10" cols="50"></textarea>

                        </td>

                  </tr>

            </table>

            <input type="submit" value="Post new message"/>

      </form>

</div>

This just add a link that when press, will show the add new post form. I am asking the user for its ID, and that isn't really nice, but I don't want to get into authentication, that is not the purpose of this post. Here is the code that handles the actual saving of the message:

public void PostMsg([DataBind("msg")] Message msg)

{

       msg.Create();

       string paginationCacheKey = string.Format("forum.{0}.messages", msg.Forum.Id);

       Context.Cache.Remove(paginationCacheKey);

       CancelView();

       Response.Redirect(string.Format("/Forum/Index.rails?forumId={0}", msg.Forum.Id));

}

How did it do that? I hear you asking. Well, it is utilizing a very smart behavior on the side of MonoRail. If you look closely, you'll notice that I naming the input fields in the form (and the query string) with an object notation. "msg.Forum.Id", for instnace. Then, all I need to do is to tell MonoRail how it show build this type for me.

Note that you do need to ensure that the values are correct. Trying to input "abc" in the Author Id will fail, but those checks are fairly easy to do. And it puts you on the "we can see the results" path a lot quicker.

After creating the new message, I clear the cache for this forum, and redirect to the main page. So, we have only two more scenarios to go (That is assuming that you are still reading this, of course).

Let us start by showing how we can display hierarchical data. We need to get all the root messages, and all their children, and so on. But right now we don't have a way to get from a Message to its children, or from a root message to all its hierarchy. Seems like we have a problem in the model (let us call a meeting).

After thinking about it, I decided that the following properties need to be added:

[HasMany(typeof(Message), "Parent", "Message")]

public ISet Children

{

       get { return children; }

       set { children = value; }

}

 

[HasMany(typeof(Message), "RootParent", "Message")]

public ISet Hierarchy

{

       get { return hierarchy; }

       set { hierarchy = value; }

}

And, the Parent needs to be modified slightly:

[BelongsTo]

public Message Parent

{

       get { return parent; }

       set

       {

              parent = value;

              if(parent==null)

                     return;

              RootParent = parent.RootParent ?? parent;

       }

}

This is done to ensure that the RootParent is set properly in databinding scenarios. Now we can build the controller's action:

public void MessageDetails(int messageId)

{

      

       SimpleQuery<Message> simpleQuery = new SimpleQuery<Message>(

                     @"from Message msg left join fetch msg.Hierarchy where msg.Id = ?",

               messageId);

       Message[] messages = simpleQuery.Execute();

       if(messages.Length==0)

       {

              PropertyBag["MessageId"] = messageId;

              RenderView("NoSuchMsg");

              return;

       }

       PropertyBag["message"] = messages[0];

}

Now you may begin to see what is the value in the RootParent, it allows us to pull all the message hierarchy efficently. We get it from the database, check that the value exists in the database, and then populate the PropertyBag with the value. Now, to the view, which is a little complicated:

<?brail

import System.Text

import System.Collections

 

def Messages(messages as ICollection):

      output "<ul>"

      for msg in messages:

?>

            <li>Title: ${msg.Title}<br/>

            Author:  ${msg.Author.Name}<br/>

            ${msg.Content}</li><br/>"""

 

<?brail output AjaxHelper.LinkToFunction('Post reply...', "$('newMsg.${msg.Id}').toggle()") ?>

<div id="newMsg.${msg.Id}"

      class="postNewMsg"

      style="display: none;">

      <form method="POST"

             action="/forum/postMsg.rails?msg.Forum.Id=${message.Forum.Id}&msg.Parent.Id=${msg.Id}">

            <table>

                  <tr>

                        <td>Title:</td>

                        <td><input name="msg.Title"

                                      type="text"/>

                        </td>

                  </tr>

                  <tr>

                        <td>Author Id:</td>

                        <td><input name="msg.Author.Id"

                                      type="text"/>

                        </td>

                  </tr>

                  <tr>

                        <td>Content:</td>

                        <td></td>

                  </tr>

                  <tr>

                        <td colspan="2">

                              <textarea name="msg.Content" rows="10" cols="50"></textarea>

                        </td>

                  </tr>

            </table>

            <input type="submit" value="Post new message"/>

      </form>

</div>

<?brail

            if msg.Children.Count >0:

                  Messages(msg.Children)

            end

      end

      output "</ul>"

end

?>

<h2>Message ${message.Id}</h2>

Title: ${message.Title}<br/>

Author:  ${message.Author.Name}<br/>

${message.Content}

 

<?brail  Messages( message.Children ) ?>

Most of this is HTML, though. What is happening is that we output the first message, and then recursively all its children, with a reply link that will open in place. This seems to the end of it, we have finished implementing all the scenarios.

Wow, that took a lot of time to write. I usually manage to finish those in a few hours, but this took several days. I hope that I managed to show you a bit of how you can use Castle to make your life easier.
This is not a real application, it is not even a pretty demo. I focused mainly on try to shed some light into what Castle can do for you. Of the top of my head, this application need a lot more UI, a bit more error handling, but seems to be it.
It is already well structured and it is well tested. Very good points to start with. :-)
time to read 122 min | 24352 words

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:

(Image from clipboard).png

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:

(Image from clipboard).png

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.Title , "Could not get title");

       Assert.AreEqual("Hello World", msgFromDb.Content, "Could not get content");

       Assert.AreEqual(user.Id, msg.Author.Id, "Author do not match" );

       Assert.AreEqual(forum.Id,  msg.Forum.Id, "Forum do not match");

      

       Assert.IsNull(msg.Parent, "Got a non null parent for a root message");

       Assert.IsNull(msg.RootParent, "Got a non null root parent for a root message");

}

Which is implemented by:

[ActiveRecord]

public class Message : ActiveRecordBase<Message>

{

       int id;

       string title;

       string content;

       User author;

       Forum forum;

       Message parent;

      

       /// <summary>

       /// This is the first message in a discussion thread

       /// </summary>

       Message rootParent;

      

       public Message()

       {

 

       }

 

       public Message(string title, string content, User user, Forum forum, Message parent)

       {

              this.title = title;

              this.content = content;

              this.author = user;

              this.forum = forum;

              this.parent = parent;

       }

 

       [Property]

       public string Title

       {

              get { return title; }

              set { title = value; }

       }

 

       [Property]

       public string Content

       {

              get { return content; }

              set { content = value; }

       }

 

       [BelongsTo]

       public User Author

       {

              get { return author; }

              set { author = value; }

       }

 

       [BelongsTo]

       public Forum Forum

       {

              get { return forum; }

              set { forum = value; }

       }

 

       [BelongsTo]

       public Message Parent

       {

              get { return parent; }

              set { parent = value; }

       }

 

       [BelongsTo]

       public Message RootParent

       {

              get { return rootParent; }

              set { rootParent = value; }

       }

 

       [PrimaryKey]

       public int Id

       {

              get { return id; }

              set { id = value; }

       }

}

Now, I can think of two scenarios that I didn't cover. First, the message should automatically set the root parent property if is it passed a non null parent, and it should set its root parent to the parent's root parent if that is not empty. Here are the tests:

[Test]

public void Ctor_Sets_Parent_And_Root_Parent_If_Not_Null()

{

       Message parent = new Message("Hi there", "Hello World", user, forum, null);

       Message msg = new Message("Hi there", "Hello World", user, forum, parent);

 

       Assert.AreEqual(parent, msg.Parent, "did not set parent correctly");

       Assert.AreEqual(parent, msg.RootParent, "did not set root parent correctly");

}

 

[Test]

public void Ctor_Sets_RootParent_To_Parent_Root()

{

       Message grandParent = new Message("Hi there", "Hello World", user, forum, null);

       Message parent = new Message("Hi there", "Hello World", user, forum, grandParent);

       Message msg = new Message("Hi there", "Hello World", user, forum, parent);

 

       Assert.AreEqual(parent, msg.Parent, "did not set parent correctly");

       Assert.AreEqual(grandParent, msg.RootParent, "did not set root parent correctly");

}

And their implementation, which is merely this addition to the constructor:

public Message(string title, string content, User user, Forum forum, Message parent)

{

       this.title = title;

       this.content = content;

       this.author = user;

       this.forum = forum;

       this.parent = parent;

       if(parent!=null)

       {

              this.rootParent = parent.RootParent ?? parent;

       }

}

Now, we need to be able to load messages for a forum, let us try:

[Test]

public void Can_Load_Messages_By_Forum()

{

       Message grandParent = new Message("Hi there", "Hello World", user, forum, null);

       grandParent.Create();

       Message parent = new Message("Hi there", "Hello World", user, forum, grandParent);

       parent.Create();

       Message msg = new Message("Hi there", "Hello World", user, forum, parent);

       msg.Create();

      

       Flush();

 

       Message[] messages = Message.FindAllByForum(forum);

       Assert.AreEqual(3, messages.Length );

}

And its implementation:

public static Message[] FindAllByForum(Forum forum)

{

       return FindAll(Expression.Eq("Forum", forum));

}

Now we only have to handle of finding all the last root messages in a forum:

[Test]

public void Can_Load_Last_Ten_Root_Messages()

{

       CreateNestedMessages();

      

       Flush();

       CreateScope();

 

       Message[] message = Message.FindLastRootsByForum(forum, 10);

 

       Assert.AreEqual(10, message.Length);

}

private void CreateNestedMessages()

{

       for (int i = 0; i < 12; i++)

       {

              Message grandParent = new Message("Hi there", "Hello World", user, forum, null);

              grandParent.Create();

              for (int j = 0; j < 2; j++)

              {

                     Message parent = new Message("Hi there", "Hello World", user, forum, grandParent);

                     parent.Create();

                     for (int k = 0; k < 1; k++)

                     {

                           Message msg = new Message("Hi there", "Hello World", user, forum, parent);

                           msg.Create();

                     }

              }

       }

}

The implementation is ridiciously short, actually:

public static Message[] FindLastRootsByForum(Forum forum, int count)

{

       return SlicedFindAll(0, count,

                                    Expression.IsNull("RootParent"),

                                    Expression.Eq("Forum", forum));

}

This is it! We now have ten tests, and a working domain model we can start using. A couple of things before we continue:

  • We dealt 99% of the time with tests / code
  • Except for the connection string, there was no need to handle configuration / references and other stuff that always clutter this examples.

Now, we want to create a the UI. For this, please create a new Castle MonoRail Project, and name it Castle.Forums.Web, you should then see the following dialog:

(Image from clipboard).png

Choose Brail (of course :-) ) and enable routing, we will use this a bit later, but let us be prepared. We don't need Windsor intergration, since this is a very small project. Admire the artwork for a while before pressing next.

(Image from clipboard).png

I don't really need to tell you that you are going to create a test project, right? Click finish and let us take a look at the projects that were created.

(Image from clipboard).png

Notice how structured this is? We already have the foundations of creating something really nice. Set the Castle.Forums.Web project to the startup project and run it. The ASP.Net Development Server will open, and you will see the boring default page.

I need to be able to work with users (since they are mandatory for everything else), but I don't want to invest any time in it at the moment. Let us just setup scaffolding. Here we need to do a bit of manual work:

Add the following references (all exist in the .NET tab in the Add References dialog) to the Castle.Forums.Web:

  • Castle.ActiveRecord
  • Castle.Components.Common.TemplateEngine
  • Castle.Components.Common.TemplateEngine.NVelocityTemplateEngine
  • Castle.MonoRail.ActiveRecordScaffold
  • Castle.MonoRail.ActiveRecordSupport
  • NHibernate

Now, add this to the <configSection> part in the web.config file:

<section name="activerecord"

               type="Castle.ActiveRecord.Framework.Config.ActiveRecordSectionHandler, Castle.ActiveRecord" />

And then add this at the bottom of the file:

<!-- For the configuration reference, check -->

<!-- http://www.castleproject.org/index.php/ActiveRecord:Configuration_Reference -->

<activerecord isWeb="true">

       <!-- The configuration below is good enough for MS SQL Server only -->

       <!-- Remember that you should use a test database, never use development or production -->

       <config>

              <add key="hibernate.connection.driver_class"

                      value="NHibernate.Driver.SqlClientDriver" />

              <add key="hibernate.dialect"

                      value="NHibernate.Dialect.MsSql2000Dialect" />

              <add key="hibernate.connection.provider"

                      value="NHibernate.Connection.DriverConnectionProvider" />

              <add key="hibernate.connection.connection_string"

                      value="Data Source=.;Initial Catalog=test;Integrated Security=SSPI" />

       </config>

</activerecord>

Now, go to the GlobalApplication.cs file and add the following to the Application_OnStart method:

IConfigurationSource source = ActiveRecordSectionHandler.Instance;

 

ActiveRecordStarter.Initialize(System.Reflection.Assembly.Load("Castle.Forums.Model"), source);

 

ActiveRecordStarter.CreateSchema();

Please note that the last line here will rebuild the database everytime that the application starts. This is great for testing / development, not so great if you are going to production, be aware of this.

We need to add a controller for the users, we will call it UserController and it looks like this:

[Layout("default"), Rescue("generalerror"), Scaffolding(typeof(User))]

public class UserController : Controller

{

}

Now that we have everything setup the way we want to, run the project, and then change the url from "/home/index.rails" to "/user/list.rails", you should see the following:

(Image from clipboard).png

It is not pretty, but it is functional, and it allows us to immediately start working with the data. Tomorrow, I'll post part II, and I'll try to discover how one can TDD a web application using MonoRail.

FUTURE POSTS

  1. Partial writes, IO_Uring and safety - 13 hours from now
  2. Configuration values & Escape hatches - 4 days from now
  3. What happens when a sparse file allocation fails? - 6 days from now
  4. NTFS has an emergency stash of disk space - 8 days from now
  5. Challenge: Giving file system developer ulcer - 11 days from now

And 4 more posts are pending...

There are posts all the way to Feb 17, 2025

RECENT SERIES

  1. Challenge (77):
    20 Jan 2025 - What does this code do?
  2. Answer (13):
    22 Jan 2025 - What does this code do?
  3. Production post-mortem (2):
    17 Jan 2025 - Inspecting ourselves to death
  4. Performance discovery (2):
    10 Jan 2025 - IOPS vs. IOPS
View all series

Syndication

Main feed Feed Stats
Comments feed   Comments Feed Stats
}