I 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.
1: <configuration>
2: <connectionStrings>
3: <remove name="LocalMySqlServer"/>
4: <add name="LocalMySqlServer" connectionString="server=localhost;uid=root;database=test;pooling=false"/>
5: </connectionStrings>
6: <system.web>
7: <membership defaultProvider="MySQLMembershipProvider">
8: <providers>
9: <remove name="MySQLMembershipProvider"/>
10: <add name="MySQLMembershipProvider"
11: type="MySql.Web.Security.MySQLMembershipProvider, MySql.Web, Version=5.1.3, Culture=neutral, PublicKeyToken=c5687fc88969c44d"
12: connectionStringName="LocalMySqlServer" enablePasswordRetrieval="false"
13: enablePasswordReset="true" requiresQuestionAndAnswer="true"
14: applicationName="/" requiresUniqueEmail="false" passwordFormat="Hashed"
15: maxInvalidPasswordAttempts="5" minRequiredPasswordLength="7"
16: minRequiredNonalphanumericCharacters="1" passwordAttemptWindow="10"
17: passwordStrengthRegularExpression=""/>
18: </providers>
19: </membership>
20: </system.web>
21: </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 accessible.
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.
1: protected override void LoadStaticConfiguration()
2: {
3: base.LoadStaticConfiguration();
4:
5: ConnectionStringSettings css = new ConnectionStringSettings();
6: css.ConnectionString = String.Format(
7: "server={0};uid={1};password={2};database={3};pooling=false",
8: BaseTest.host, BaseTest.user, BaseTest.password, BaseTest.database0);
9: css.Name = "LocalMySqlServer";
10: Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
11: config.ConnectionStrings.ConnectionStrings.Add(css);
12:
13: MembershipSection ms = (MembershipSection)config.SectionGroups["system.web"].Sections["membership"];
14: ms.DefaultProvider = "MySQLMembershipProvider";
15: ProviderSettings ps = new ProviderSettings();
16: ps.Name = "MySQLMembershipProvider";
17: Assembly a = Assembly.GetAssembly(typeof(MySQLMembershipProvider));
18: ps.Type = "MySql.Web.Security.MySQLMembershipProvider, " + a.FullName;
19: ps.Parameters.Add("connectionStringName", "LocalMySqlServer");
20: ps.Parameters.Add("enablePasswordRetrieval", "false");
21: ps.Parameters.Add("enablePasswordReset", "true");
22: ps.Parameters.Add("requiresQuestionAndAnswer", "true");
23: ps.Parameters.Add("applicationName", "/");
24: ps.Parameters.Add("requiresUniqueEmail", "false");
25: ps.Parameters.Add("passwordFormat", "Hashed");
26: ps.Parameters.Add("maxInvalidPasswordAttempts", "5");
27: ps.Parameters.Add("minRequiredPasswordLength", "7");
28: ps.Parameters.Add("minRequiredNonalphanumericCharacters", "1");
29: ps.Parameters.Add("passwordAttemptWindow", "10");
30: ps.Parameters.Add("passwordStrengthRegularExpression", "");
31: ms.Providers.Add(ps);
32:
33: config.Save();
34: ConfigurationManager.RefreshSection("connectionStrings");
35: ConfigurationManager.RefreshSection("system.web/membership");
36: }
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.