Thursday, September 20, 2007

NUnit testing with dynamic configuration files

My use NUnit for all my unit tests in Connector/Net. Over the past few years I've added a few features to my test suite library to make testing easier. One of the better ones I implemented was test subclasses. I had been looking for a way to run my test fixtures multiple times against the same host/database but with different
configuration options. I didn't want to restart the test process because I wanted to collect coverage stats without having to resort to merging multiple output files. Test subclasses handled that nicely.
My most recent test related task was writing unit tests for the web providers we include in Connector/Net 5.1. I had already written some tests but they really needed to be run manually and they included an app.config file that would have to be changed with every release. Here is the app config file I am talking about.

<configuration>
   <connectionstrings>
     <remove name="LocalMySqlServer">
     <add connectionstring="server=localhost;uid=root;database=test;pooling=false" name="LocalMySqlServer">
   </connectionstrings>
   <system.web>
     <membership defaultprovider="MySQLMembershipProvider">
       <providers>
         <remove name="MySQLMembershipProvider">
         <add applicationname="/" connectionstringname="LocalMySqlServer" enablepasswordreset="true" enablepasswordretrieval="false" maxinvalidpasswordattempts="5" minrequirednonalphanumericcharacters="1" minrequiredpasswordlength="7" name="MySQLMembershipProvider" passwordattemptwindow="10" passwordformat="Hashed" passwordstrengthregularexpression="" requiresquestionandanswer="true" requiresuniqueemail="false" type="MySql.Web.Security.MySQLMembershipProvider, MySql.Web, Version=5.1.3, Culture=neutral, PublicKeyToken=c5687fc88969c44d"/>
       </providers>
     </membership>
   </system.web>
 </configuration>

As you can see, this config file directly references not only the testing host and database in the LocalMySqlServer connection string but it directly references version 5.1.3 of MySql.Web (the assembly that houses our providers). I like this to be very automatic so this will never do.

I already have config files that I use for testing against MySQL 4.1, 5.0, and 5.1. All that is included in these files are the port number, named pipe name, and shared memory name (and I could remove these last two and generate them dynamically now that I think about it). I wanted to reuse these config files for my web testing so I started writing code to read these config values in. However I then realized that I had already done this with the base test class in my core assembly test suite. So rather than rewrite that, I added a virtual method named LoadStaticConfiguration (so web tests could extend it) and made the base test publicly acessible.

Now my web test library includes a class named BaseWebTest that extends BaseTest. At this point, all my web testing fixtures inherit the config reading and dynamic database setup that is in the base testing library. However, there was still a problem. My server testing config files didn't include any connection string and membership provider registration (and I didn't want to add it).
Let me explain why these parts are important. The web providers are assemblies that integrate into an existing provider framework provided by .NET. There are static methods that a user can call such as 'Membership.CreateUser(…)' that will use the information found in the config file to know what providers to instantiate and what connection strings to use. I needed to be able to test this type of behavior so the config files are important. Still, I wasn't about to hand update my config files on every product release.

So I decided what I needed was to dynamically update the config system when the test starts up. There wasn't a lot of information in the blogosphere on how to do this so that's why I'm writing this. Here you see my implementation of LoadStaticConfiguration.

protected override void LoadStaticConfiguration()
    {
        base.LoadStaticConfiguration();
       
        ConnectionStringSettings css = new ConnectionStringSettings();
        css.ConnectionString = String.Format(
            "server={0};uid={1};password={2};database={3};pooling=false",
            BaseTest.host, BaseTest.user, BaseTest.password, BaseTest.database0);
        css.Name = "LocalMySqlServer";
        Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
        config.ConnectionStrings.ConnectionStrings.Add(css);
      
        MembershipSection ms = (MembershipSection)config.SectionGroups["system.web"].Sections["membership"];
        ms.DefaultProvider = "MySQLMembershipProvider";
        ProviderSettings ps = new ProviderSettings();
        ps.Name = "MySQLMembershipProvider";
        Assembly a = Assembly.GetAssembly(typeof(MySQLMembershipProvider));
        ps.Type = "MySql.Web.Security.MySQLMembershipProvider, " + a.FullName;
        ps.Parameters.Add("connectionStringName", "LocalMySqlServer");
        ps.Parameters.Add("enablePasswordRetrieval", "false");
        ps.Parameters.Add("enablePasswordReset", "true");
        ps.Parameters.Add("requiresQuestionAndAnswer", "true");
        ps.Parameters.Add("applicationName", "/");
        ps.Parameters.Add("requiresUniqueEmail", "false");
        ps.Parameters.Add("passwordFormat", "Hashed");
        ps.Parameters.Add("maxInvalidPasswordAttempts", "5");
        ps.Parameters.Add("minRequiredPasswordLength", "7");
        ps.Parameters.Add("minRequiredNonalphanumericCharacters", "1");
        ps.Parameters.Add("passwordAttemptWindow", "10");
        ps.Parameters.Add("passwordStrengthRegularExpression", "");
        ms.Providers.Add(ps);
      
        config.Save();
        ConfigurationManager.RefreshSection("connectionStrings");
        ConfigurationManager.RefreshSection("system.web/membership");
    }

Lines 5-11 add our connection string to the connection strings section. Note how we are using String.Format along with the base class defined statics host, user, password, and database0. This one was easy.

Lines 13-31 add our membership provider to the system.web section. The only tricky part is that you have to index into the SectionGroups array with the name "system.web" and then index into that groups Sections array with the name "membership" and then cast the return value to a MembershipSection. Once you have this you can just new up a ProviderSettings class, fill in all the values, and add it to the providers array on the membership section object. One area to note is lines 17 and 18. Remember we want this to be completely dynamic so we use Assembly.GetAssembly to get a reference to the assembly that includes our provider (not ExecutingAssembly since we are currently executing inside the testing assembly), grab it's full name, prefix on our provider class name and use that for our provider type.

Line 33 tells the configuration system to save itself. At this point you might think things should work but they won't. You've changed the values on disk but not what's cached in memory. Lines 34 & 35 take care of that. They simply instruct the configuration system that the next time values from the "connectionStrings" or "system.web/membership" sections are accessed they should be reread from disk.

That's it! Now my web testing framework will work even without a config file. All you have to do is point it at a running mysql instance.

No comments:

Post a Comment