So, you want to upgrade your app to Grails 2.4.4 from that legacy 1.3.7 version today!
But wait! There are few challenges waiting for you in the process.
Thinking, from where should we start upgrading, as it was quite a task for such an enterprise application upon which whole business is running. Any bug introduced in the process and you are toast! But here comes the tests to rescue us! You may want to ask now what kind of tests? Please wait till I come to tests. I'll explain them, I promise.
Lets start, with some theory and forth coming problems!
-
Upgrading an app is just not about changing the version number in the
application.properties
file. It's more than that! You've got 'n' number of plugins, some of them you've made in-house which are served from your repository and hosted by your centrally running Artificatory instance! And of course the plugins from Maven have their own story to speak up. There is more to plugins, remember inplace plugins? you have to upgrade them as well. -
Next is the time, you realize that your code is misbehaving and you don't have any idea about what happened! Means, you just know in which domain the app is operating but you have no clue what are the relationships established between objects by the developers and you need to be sure if these relationships are intact after upgrade (Here you are asked to upgrade someone else's app).
-
You've got different environment settings and config properties in
BuildConfig.groovy
andConfig.groovy
files. They would need your special attention now, by saying so I mean the versions i.e. checking for their compatibility. -
Your app is utilizing
Grails' i18n
system and users are managed according to their locale. You know there is a lot of work going on here, but rest assure as Grails andjava.util.Locale
class has your back. -
You've a very large
UrlMappings.groovy
file which contains the enormous entries of urls, and if that's not enough you've got them in different localized versions!
These were some of the problems I just mentioned.
Every new version of Grails provides some new features improvements and update to old or deprecate features i.e., TagLibs, GSPs, Controllers get some new enhancements with every new release. Among these most notable improvements are Tests.
Since there were quite major changes between Grails version 1.3.7 and 2.4.4 e.g., ApplicationHolder
, ConfigurationHolder
were deprecated in 2.2.4 but were removed in 2.4.4, etc. So, instead of jumping directly to most recent version at that time (2.4.4), we started a series of upgrades from 1.3.7 to 2.2.4 and then to 2.4.4. this way we insured that our application is not breaking and thus saved ourself from disasters.
The app which we just upgraded had almost 100% test coverage which included unit, integration and functional tests. Here are few things that should be kept in mind while working with prior written tests cases:
-
Well, things keep on changing and so does our testing frameworks, however their working doesn't, thanks to the Abstraction concept, which allows us to continue using something regardless of inner working and utilizing the same interface and syntax, but not the semantics, of course.
-
You should not on any cost alter the assertions of the tests (or specifications as we say them in Spock), whether these are unit, integration or functional. If tests are failing then there is some flaw introduced during upgrade or the previous version code simply doesn't work any more (I'll show an example regarding it later). This point is extremely important to have an upgraded app that works as it should (you mark my words on it)!
-
I heard that we have functional tests also! What are they? You may ask. These are Selenium tests running in disguise of Geb, to automate the browser, so that you don't have to check it manually every time, e.g., Is login still working? Just execute some Geb test and it shall verify for you. One more note here is that these tests only work with specific version of browsers. For our case we had to use Firefox 32.0.1, (you can also use Chrome). Of course, it entirely depends on the version of Selenium library and the driver.
So, it's time that we actually upgrade the app by leaving behind our observations but sticking to them all the time.
Here I'll describe the initial steps to be taken in order to upgrade the app. These are essential and no one should lost in explanation.
-
You might want to start with tweaking the values in your
Config.groovy
andDataSource.groovy
file. Well, instead of modifying these files you are encouraged to provide your own external config file likemy-config.groovy
and adding it tograils.config.locations
property inConfig.groovy
. The configuration in this external file will then override your configuration inConfig.groovy
andDataSource.groovy
file. I'm recommending external config file because everyone can have their own settings in the same file. Of course this file should not be committed to the repository and must be in ignore list of theVCS
. This is a nice idea. Is it not? -
One most notable configuration I found useful was to enable reloading of GSP changes. Grails usually does the trick automatically, but in my case it broke. If it's the case with you also then try adding these properties in your external config file:
grails.gsp.enable.reload = true grails.reload.enabled = true
-
Next we should start upgrading dependencies in our
BuildConfig.groovy
file. Find the dependencies on Maven repository which are compatible with currentGrails
release, grails plugins directory would provide a great help on this. Basically, what we are trying to achieve here is that on upgrading the app we would have all our dependencies and plugins resolved, so that any code using those won't break/die at compile time.
As I said earlier, I would talk about tests and explain why are they so much needed. I won't talk about TDD specifically as the code and tests have already been written beforehand.
-
Unit tests
Are minimal of them, although they can't promise the whole integrity of the app because they are used to test only a unit of work e.g., methods and classes. And I said that because there's a saying -- 100% unit test coverage doesn't guarantee a working app, however your modules would be working individually. Regardless it, unit tests are a necessity.
In Grails, unit tests are mainly supported by Spock. The working of tests from version 1.3.7 to 2.4.4 has changed much in respect of syntax and as well as in semantics. So to fix the tests we should use new
Annotations
and behavior provided by the testing framework, and you're good to go. e.g., To test aService
class sayMyService.groovy
you would go as follows:// Class under test class MyService { def someMethod() { return "returning a string." } } // Test written with version 1.3.7 import grails.plugin.spock.Unitspec class MyServiceUnitSpec extends UnitSpec { def myService def "testing someMethod"() { when: def result = myService.someMethod() then: "returning a string." == result } } // Now same test written with version 2.4.4 import spock.lang.Specification @TestFor(MyService) class MyServiceUnitSpec extends Specification { def "testing someMethod"() { when: def result = service.someMethod() // Notice the service object! then: "returning a string." == result } }
As we can see here we upgraded our test to newer version. For this we had to change the semantics, changing the way how we now approach the respective class under test.
-
Integration tests
You can follow the same steps mentioned above for unit tests. As we all know, integration test run a particular feature in whole. So, they behave more or less like unit tests.
-
Functional tests
Functional tests are much different from our unit and integration tests, both in functionality and implementation part. We use a separate library call Geb to implement these tests. Geb basically, handles the pages which are shown on the UI. It helps us to automate the page behavior, e.g., Does that pop-up box open with certain content inside it, if we click that button without filling the required textbox on product search page? The questions like these needs to be manually tested/answered every time whenever we change our code, and it's where functional tests shine. Geb tests require some extra time and needs to be updated whenever our view logic changes, but investment is future proof!
Keep in mind that to check if functional tests are working properly we need to run the app, and if anything breaks in test, it's either fault in code or in test itself. To determine where does the fault lie, here are some quick tips:
- Is there any element missing on the page or its
id
orclass
attribute has changed, and you are not able to identify the element? It may be due tochange in our css rule definition
orrefactoring
. - Are we testing any
div
orspan
for visibility? It's a common source of failing tests as the required value doesn't simply get filled inside the element under test. - Sometimes it's the missing
waitFor
method that is causing the test to fail. - You must use
Browser.via()
instead ofBrowser.to()
, iff your page uses redirection.
These were some guidelines, but none of them were related to the upgrade process of our app. So, lets turn up to them now:
- The very first problem we usually face -- What data is being passed from every
controller.action
to the view. Before we fix this, we should address one more problem. As there could be manycontroller.action
redirects, it is quite hard to find correctcontroller.action
executing right now. Moreover, thesecontroller.action
were heavily mapped usingUrlMappings.groovy
file. Seeing the problem? So one solution I found was to create aFilter
that would list everycontroller
,action
,params
and resultingmodel
on eachcontroller.action
call/request.
Of course, I could have used an already created filter, but that would be overkilling! So, I just created a new
Filter
as follows:// Prints every route and associated data class AppDebugFilters { def filters = { all (controller: '*', action: '*') { before = { log.debug "..........AppInfoFilters Start........" log.debug "$controllerName/$actionName : $params" } after = { Map model -> log.debug "$model" log.debug "..........AppInfoFilters End.........." } afterView = { Exception e -> // Don't need it } } } }
Watchout!!! Make sure you remove this filter when you deploy on production, as, it would printout every detail in logs!
- Now we are able to detect which
url
is being hit by the app. But another point is what if some assertion fails in your test? As I said, this is a case where your current code no longer works after the upgrade. Suppose you've got a sitation as follows in GSPs and inside our Geb test we are counting elements based on applied css class:
// BabyController.groovy class BabyController { def babbies() { def baby = new Baby() [newBornBaby: baby] // There could be other parameters also e.g., all the babbies in system currently. } } // babbies.gsp <g:render template="list" model="[baby:newBornBaby]" /> // _list.gsp <g:render template="baby" /> // _baby.gsp <g:set var="clazz" value="${baby ? 'cute' : 'very cute'}" />
Can you guess now what would be the effect of using
baby
inside_baby.gsp
, which was not explicitly passed to it and the value of theclazz
variable? Here is the answer:clazz
will always containcute
as its value. Why? Becausebaby
model key was defined to be aBaby
object, and in this case it's nevernull
. You got the point, right?So, what's the solution? It's as simple as that you pass your models explicitly as follows:
// _list.gsp <g:render template="baby" model="[baby: false]" />
- This step discusses about configuring the
Geb
itself so that you would be in position to test the app manually when browser opens, as normally browsers are tend to get shutdown after a failed or successful test. So make sure, that we never add aquit()
method call as follows:
FirefoxDriver driver def setupSpec() { driver = new FirefoxDriver(new FirefoxProfile()) } def cleanupSpec() { driver.quit() // Never do this! }
Even, if this
driver.quit()
line is written somewhere, then just comment it out. The importance of removing this statement is, it would preserve the state of the browser session and you would be able to manually inspect the elements on the page. You would also be able to answer the questions like, if there is something wrong going on with theAJAX
call (which is made on click of a button), by just checking it inside the browser console. - Is there any element missing on the page or its
So it was all about testing and upgrading (okay, not everything!). To upgrade an app is really not a simple task, and when you don't have idea about the code but except the logic, situation becomes really complex. In this case you are just relied on tests, whether they are unit
, integration
or functional
.
You would be thinking that I'm little biased towards testing. Actually I'm. To test the app or some feature every time manually is not something that I say is wise decision. Suppose, I changed some code in one part of app. Now how can I be sure if some other module in the app is not affected due to this change or affected altogether? Just automate the things using tests and you are good to go. There were situations when I modified some code to get pass a test but due to the change I made some other tests started to fail. This way I didn't have to test the app again (manually), instead tests provided me feedback. Seeing the power of tests!
People usually say they have 100% unit
tests, and should they still need integration
and functional
tests? And answer is, YES. Unit tests only guarantee that every unit would work fine. As an app works with individual units combined together they should be tested as a group. And this is where integration tests helps us. You might say then why functional tests? Functional tests are necessary to see if software is working as the user is asking it to. Finding the difference between integration and functional? Integration tests are just to ensure all units are working together to produce desired results, and functional tests are just to ensure software is working as per the user requirements and desired behavior.
If someone comes here and ask me, will my app be working fine if I've all the tests (unit, integration and functional) passing? And my experience says, YES!
So, it was about the upgrading of a Grails app. This might not be the complete checklist but you got the idea where to tackle the problem and come up with a solution.
Thanks for joining in the conversation.