Javascript strings translation with gettext
In a previous post I mentioned a few ways of dealing with i18n of strings in ASP.NET pages with gettext. Now it’s time for javascript code.
Although javascript code in ASP.NET pages can be internationalized using the <%= %> syntax, pure js files are not processed, and therefore a different mechanism for dealing with javascript is needed.
A quick google search will return multiple results. BerliOS javascript gettext implementation provides an interface with the same methods as the ones provided by any other port. It requires that the content of the translation files is available as a dictionary visible to the script.
Note that it is not necessary to have a full implementation of gettext client side. Having a simple function for translation, checking a simple dictionary will do:
Resources.T = function(str) { return Resources[str] || str };"
The Resources dictionary can be easily passed to the client via JSON, by retrieving the full dictionary on the server and encoding it.
An efficient approach for including this data is having a server endpoint returning this dictionary as json and including it as a referenced script in all master pages. This will cause the browser to download the full set of resources the first time and then used it cached version; therefore it is not necessary to send the full load of strings on every request. Make sure to add the culture of the requested resource to the URL, so when a user changes his/her language the new resource set is loaded.
As for .po generation, xgettext seems to crawl javascript code without too many issues, so this point should not pose any problems.
On the other hand, the gettext-js library provides client side methods for parsing .po files. Therefore, it is not necessary to add a server endpoint with the intelligence to convert the po file into its json representation: the library will take care of this by itself, just point itself to an URL containing the file to parse and voila!
Note, however, that since the po file may have some metadata which is used only during translation time (such as in which files is the string referenced, the format, etc), its size may actually double or triple its json representation, making it more expensive to be sent to the client.
Also, conversion of the .po files into json can be done a single time in the server and sent to every client, instead of having each of them spend extra browser processing time parsing and processing.
Either approach you take, gettext remains an excellent tool for internationalizing your web application.
Parameterized Tests
In order to prevent the dreadful copy-paste habits in unit tests (remember, tests are also code, so all the good practices you use when writing code should also apply to tests) a common pattern to test similar behaviour when slightly changing the input is to extract the test itself into a separate method.
The scenario
Suppose we want to test the behaviour of a class in charge of setting a user’s position based on supplied lat/long values.
interface IUserLocator { bool Place(User user, double latitude, double longitude); }
Doing it the wrong way
A typical fixture covering some invalid scenarios, developed by a copy paste fan, would look like the following:
[Test] public void ShouldFailLatitudeLessThanNeg90() { bool result = locator.Place(user, -100, 20); Assert.False(result); Assert.AreEqual(previousLatitude, user.Lat); Assert.AreEqual(previousLongitude, user.Long); } [Test] public void ShouldFailLatitudeMoreThan90() { bool result = locator.Place(user, 100, 20); Assert.False(result); Assert.AreEqual(previousLatitude, user.Lat); Assert.AreEqual(previousLongitude, user.Long); } [Test] public void ShouldFailLongitudeLessThanNeg180() { bool result = locator.Place(user, 20, -190); Assert.False(result); Assert.AreEqual(previousLatitude, user.Lat); Assert.AreEqual(previousLongitude, user.Long); }
Refactoring
After the 3rd copy-pasted method, it should be pretty obvious that our code is demanding to extract the common code into a separate method which executes the body of the test. After this refactor we would have:
private void ShouldFailInvalidLatLong(double lat, double lng) { bool result = locator.Place(user, lat, lng); Assert.False(result); Assert.AreEqual(previousLatitude, user.Latitude); Assert.AreEqual(previousLongitude, user.Longitude); } [Test] public void ShouldFailLatitudeLessThanNeg90() { ShouldFailInvalidLatLong(-100, 20); } [Test] public void ShouldFailLatitudeMoreThan90() { ShouldFailInvalidLatLong(100, 20); } [Test] public void ShouldFailLongitudeLessThanNeg180() { ShouldFailInvalidLatLong(20, 190); }
Which is a lot better than before. However, we are forced to keep a different method for each of the different parameter combinations we are injecting into the auxiliary testing function.
We could keep all of them in a single method and loop through a list of parameters configurations, but in this case NUnit would not be able to distinguish them as different tests and fail the whole set if a single one does. And even worse, we don’t get a SetUp method for every case unless we call it explicitly, which is a huge drawback.
Injecting parameter values
Luckily, NUnit included native support for handling lists of parameters in version 2.5. Now we can simply write:
[Test] [Sequential] public void ShouldFailInvalidLatLng( [Values(-100, 100, 20)] double lat, [Values(20, 20, 190)] double lng) { bool result = locator.Place(user, lat, lng); Assert.False(result); Assert.AreEqual(previousLatitude, user.Latitude); Assert.AreEqual(previousLongitude, user.Longitude); }
The Values attribute applied to a parameter lets us define which set of values we want to inject into that parameter for testing. NUnit will simply iterate through all the values, generating a different test for each of them.
Note the Sequential attribute applied to the test. By default, NUnit will generate all possible combinations of the parameters’ values; in this case, we want a sequential approach to only generate 3 tests: one with the first set of values (-100, 20), another one with (100, 20) and the last one with (20, 190). Another option is to use the Pairwise attribute, which is a restricted variation of the combinatorial, that guarantees that every pair of different parameters will be executed, which is useful to control the amount of tests generated when handling multiple input parameters; however, it is not what we want in this case.
Now, this is how it looks like when ran in the console:
It is even clearer than its verbose counterpart when you look at them together, as its groups all the instances of the same method under the same treenode.
Generating parameter values
Besides handling parameter combinations, NUnit also offers multiple options for injecting parameter values, such as the Range and the Random attribute, which generate values inside a specified range with a certain step (in the former) or in a completely random fashion (in the latter).
The following example will generate four different values for latitude, and three different random values for longitude, all of them in a valid range.
[Test] [Combinatorial] public void ShouldSetLatLng( [Range(-90, 90, 60)] double lat, [Random(-180, 180, 3)] double lng) { bool result = locator.Place(user, lat, lng); Assert.That(result); Assert.AreEqual(lat, user.Latitude); Assert.AreEqual(lng, user.Longitude); }
Whenever we reload the test suite, NUnit will generate new fresh values for the randomized parameter. Losing determinism in your test suite is not something to overlook, so you should actually think twice before adding a random attribute to a parameter.
On the right is the tests generated as output of the test case above. Note that with very little effort the values generators, paired with the combinatorial attribute, produce a considerable number of test cases.
Extracting common data
Yet another way to supply values is, if you don’t the noise generated by the values attribute applied on every parameter, or if you want to reuse a set of data, is the ValueSource attribute, which lets you pull data from another source, in the following fashion:
public class LocatorPoints { double[] latitudes = { -90, -60, -30, 0, 30, 60, 90 }; double[] longitudes = { -180, -90, 0, 90, 180 }; } [Test, Sequential] public void ShouldSetLatLng( [ValueSource(typeof(LocatorPoints), "latitudes")] double lat, [ValueSource(typeof(LocatorPoints), "longitudes")] double lng) { bool result = locator.Place(user, lat, lng); Assert.That(result); Assert.AreEqual(lat, user.Latitude); Assert.AreEqual(lng, user.Longitude); }
This allows you to define the data sets and the tests applied separately, and reuse sets of data whenever necessary. With this feature we are not only extracting common code, but also extracting common datasets, and generating all combinations between them with ease.
The TestCaseSource attribute
You may choose to have even more control on the data supplied (and even on the values returned by the test!) with the TestCaseSource attribute.
One way of using it is simply as a collection of value sources, specifying together all the input parameter combinations you are interested in:
public class LocatorPoints { object[] valid = { new[] { -90, -180 }, new double[] { 90, 180 }, new TestCaseData(30, 180) }; } [Test] [TestCaseSource(typeof(LocatorPoints), "valid")] public void ShouldSetLatLng(double lat, double lng) { bool result = locator.Place(user, lat, lng); Assert.That(result); Assert.AreEqual(lat, user.Latitude); Assert.AreEqual(lng, user.Longitude); }
The source of points may be a method instead of a field, which allows you to dynamically generate the parameter values you may want.
Note the way the combination (30, 180) is injected: it uses the TestCaseData class. This class grants much more control over the parameterization than only specifying the input parameters. It provides a fluent interface to set the return value, expected exception, description, name, category, etc.
Let’s see how a complete fixture, for both valid and invalid tests, would look like when written using this interface:
public IEnumerable<TestCaseData> Values() { yield return new TestCaseData(0, 0).Returns(true) .SetName("ShouldSetLatLongOrigin").SetCategory("valid"); yield return new TestCaseData(90, 180).Returns(true) .SetName("ShouldSetLatLongMaxValues").SetCategory("valid"); yield return new TestCaseData(100, 30).Returns(false) .SetName("ShouldNotSetInvalidLat").SetCategory("wronglat"); yield return new TestCaseData(20, 190).Returns(false) .SetName("ShouldNotSetInvalidLng").SetCategory("wronglng"); } [Test] [TestCaseSource("Values")] public bool SetLatLng(double lat, double lng) { bool result = locator.Place(user, lat, lng); Assert.AreEqual(result ? lat : previousLatitude, user.Latitude); Assert.AreEqual(result ? lng : previousLongitude, user.Longitude); return result; }
By using a simple method that yields different configurations for our parameterized test we can set the input values, the expected return value, a specific name, category, etc. Basically we can write nearly a whole fixture using a single test and adequate parameters.
Whether this is too much injection over the tests or not is another discussion, finding a right balance is no easy task. In the very end, it is just another way of writing tests.