Construir la interfaz de usuario (II)
Tests
Siguiendo con la idea inicial de unir en un mismo framework varias de las herramientas de código abierto más utilizadas en el mundo del desarrollo de aplicaciones, Grails optó por incluir dos de los más populares frameworks de tests existentes en la actualidad como son JUnit y Canoo. Mediante estos frameworks vamos a poder realizar tests unitarios y de integración, así como tests funcionales. El propósito de éstos tests es verificar que la aplicación funciona tal y como esperamos y confirmar que ésta no ha dejado de funcionar después de iterar sobre la misma. Como estarás pensando, en una aplicación con muchas personas trabajando, es igual de importante desarrollar una buena aplicación como crear un buen paquete de tests. Hay incluso algunas teorías con las que están de acuerdo muchos desarrolladores que afirman que los tests deben ser implementados incluso con anterioridad a la aplicación.
En primer lugar vamos a realizar algunos tests de integración para verificar que los métodos handleLogin() y logout() funcionan correctamente. Posteriormente crearemos también unos tests funcionales para comprobar la funcionalidad del encabezado de nuestra aplicación. Empecemos por los tests de integración.
Tests de integración
En la primera sesión sobre Grails ya vimos como se pueden crear tests de integración en Grails gracias al comando grails create-integration-test seguido del controlador sobre el que deseamos generar los tests. Este comando creará un fichero en el directorio test/integration llamado UsuarioTests.groovy con el siguiente contenido:
import grails.test.* class UsuarioTests extends GrailsUnitTestCase { protected void setUp() { super.setUp() } protected void tearDown() { super.tearDown() } void testSomething() { } }
Recordemos que el método setUp() se ejecutará antes que cualquier test de la clase en cuestión, mientras que el método tearDown() será lo último que se haga. La típica utilidad del método setUp() es eliminar todos los datos de la correspondiente clase de dominio para dejarla en un estado conocido.
En primer lugar, vamos a comprobar el funcionamiento del método handleLogin() y para ello vamos a crear un par de tests, uno con un usuario correcto que compruebe que el usuario puede entrar en el sistema sin problemas y otro con un usuario incorrecto que no podrá entrar en el sistema. Los métodos deberán empezar por la palabra test para que sean tenidos en cuenta cuando se pase la batería de tests. El siguiente código muestra la función testHandleLogin() que comprueba que un usuario válido consigue entrar en el sistema.
Usuario u UsuarioController uc protected void setUp() { //Creo el usuario u = new Usuario(login:'frangarcia2',password:'mipassword',nombre:'Francisco José',apellidos:'García Rico',tipo:'administrador', email:'fgarcia@ua.es') u.save() //Inicializo el controlador uc = new UsuarioController() } protected void tearDown() { u.delete() } void testHandleLogin() { // Establece los parámetros del usuario uc.params.login = u.login // Invoca la acción uc.handleLogin() // Si la acción ha funcionado correctamente, la variable session tendrá los datos del usuario def sessUsuario = uc.session.usuario //Compruebo que la sesión del usuario se ha creado correctamente //y que no es null assert sessUsuario assertEquals(u.login, sessUsuario.login) // Y el usuario se redirige a la página para realizar operaciones assertEquals "/operacion", uc.response.redirectedUrl }
Para comprobar que la aplicación pasa correctamente nuestra batería de tests, debemos ejecutar el comando grails test-app. Este comando nos mostrará por pantalla toda la información relativa a los tests indicándonos si han tenido éxito o no, pero además, nos generará un informe completo sobre ellos en el directorio test/reports. Si sólo queremos realizar los tests sobre la clase recién creada UsuarioTests podemos ejecutar también el comando grails test-app Usuario.
Ya tenemos comprobado que un usuario válido en nuestro sistema puede entrar correctamente en el mismo. Lo siguiente que vamos a hacer es crear otro test que compruebe que un usuario no válido no puede entrar en la aplicación. Para ello creamos el método testHandleLoginInvalidUser() y en él comprobaremos, al igual que hicimos en el método anterior que el usuario es redirigido a una determinada página así como el mensaje de error devuelto por el sistema.
void testHandleLoginInvalidUser() { // Establece los parámetros del usuario uc.params.login = "loginincorrecto" // Invoca la acción uc.handleLogin() //Compruebo que la acción ha redireccionado de nuevo a la página de login assertEquals "/usuario/login", uc.response.redirectedUrl //Compruebo el mensaje flash devuelto por el controlador def message = uc.flash.message assert message assert message.startsWith("El usuario ${uc.params.login} no existe") }
Volvemos a ejecutar el comando grails test-app para comprobar que todo sigue funcionando tal y como pensábamos.
Si todo ha ido bien, ya habremos realizado un test positivo (con un usuario válido) y otro negativo (con un usuario no válido) sobre nuestra aplicación para el método handleLogin(). Por último, vamos a realizar un nuevo test para comprobar que el método logout() funciona correctamente. En este test comprobaremos que al abandonar el sistema, la variable session se queda vacía y que el usuario es redirigido a la página de login para que vuelva a identificarse.
void testLogout() { // Simulamos que el usuario ya se ha identificado en el sistema copiando sus datos en la variable session uc.session.usuario = u // Abandonamos la aplicación uc.logout() def sessUsuario = uc.session.usuario //Comprueba que la variable sessUsuario es null assertNull("Expected session user to be null", sessUsuario) //Comprueba que el usuario es redirigido a la página usuario/login assertEquals "/usuario/login", uc.response.redirectedUrl }
Si ahora ejecutamos el comando grails test-app Usuario podemos comprobar si nuestros tests se han ejecutado correctamente.
A partir de ahora, lo ideal es desarrollar los tests de integración al mismo tiempo que se desarrolla la aplicación, ya que con ellos conseguiremos ahorrar mucho tiempo y esfuerzo.
Tests funcionales
Los siguientes tests que vamos a utilizar en nuestra aplicación son los llamados tests funcionales y que como su propio nombre indica pretenden comprobar la funcionalidad de la aplicación. En estos tests se pretende imitar las posibles acciones que pueden los realizar los usuarios interactuando con la aplicación tales como hacer clics sobre enlaces y enviar formularios. Hasta ahora, esto lo estamos haciendo a mano y cada vez que hacemos un cambio debemos realizar una serie de pruebas mínimas para comprobar que no hemos hecho algo incorrecto al introducir estos cambios en el código. Con los tests funcionales vamos a poder automatizar este proceso.
Grails pone a disposición de los desarrolladores un plugin de Canoo, un popular framework de código abierto para la implementación de tests funcionales (recuerda que todo los frameworks de Grails ya han sido ampliamente utilizados por otras comunidades). Este plugin nos facilitará la realización de tests funcionales para las cuatro operaciones básicas de cualquier aplicación (creación, lectura, edición y borrado).
Como hemos dicho, la posibilidad de realizar tests funcionales en Grails viene gracias a un plugin del framework Canoo. En Grails tenemos la suerte de disponer de una comunidad de usuarios cada vez mayor que desarrollan aquello que en Grails no viene por defecto mediante un sistema de plugins. A lo largo de este curso, veremos diferentes plugins que podemos utilizar en el desarrollo de nuestras aplicaciones. Tenéis una amplia referencia de todos los plugins disponibles en Grails en la dirección http://www.grails.org/plugin/home.
La instalación del plugin de Canoo es tan sencilla como ejecutar el comando grails install-plugin webtest desde el directorio de la aplicación. Grails se encarga de descargar el plugin e instalarlo para que pueda ser utilizado en la aplicación. Una vez instalado, crearemos nuestro primer test funcional que pretende comprobar que la funcionalidad básica de la aplicación cuando creamos, listamos y eliminamos usuarios se ejecuta correctamente.
Para crear el test funcional ejecutamos el comando grails create-webtest Usuario. La ejecución de este comando creará un nuevo directorio llamado webtest dentro del directorio de la aplicación con tres subdirectorios: conf, reports y tests. Dentro del directorio conf se encuentra un archivo de configuración del plugin, que en principio no se debe modificar y que básicamente contiene la siguiente información:
Nombre | Valor inicial | Descripción |
---|---|---|
wt.config.host | localhost | Nombre del servidor |
wt.config.port | 8080 | Número de puerto donde se arranca el servidor |
wt.config.protocol | http | Protocolo utilizado para la comunicación con el servidor |
wt.config.summary | true | Indica si se debe imprimir un resumen del informe |
wt.config.saveresponse | true | Indica si se debe guardar la respuesta en el informe |
wt.config.resultpath | webtest/reports | Indica el lugar donde se deben alojar los informes |
wt.config.resultfile | WebTestOverview.xml | Indica el nombre del fichero con el informe |
wt.config.haltonerror | false | Indica si se debe parar la ejecución del test en caso de que se encuentre un error |
wt.config.errorproperty | webTestError | Indica el nombre de la propiedad Ant cuando se produce un error |
wt.config.haltonfailure | false | Indica si se debe parar la ejecución del test en caso de que se encuentre un fallo |
wt.config.failureproperty | webTestFailure | Indica el nombre de la propiedad Ant cuando se produce un fallo |
wt.config.showhtmlparseroutput | true | Indica si se deben mostrar los errores y los fallos parseados en la consola |
Otro fichero generado automáticamente al crear el test funcional es webtest/tests/TestSuite.groovy que usa la utilidad de Ant fileScanner para cargar todas las clases que se alojen en el directorio webtest/tests y que terminen con la palabra Test. Una vez estas clases han sido cargadas, se ejecuta el método suite() para cada una de estas clases.
Ahora que ya tenemos instalado el plugin de Canoo para la realización de tests funcionales, vamos a ver como quedaría nuestro primer test funcional. Para ello, cuando ejecutamos grails create-webtest Usuario, Grails creo un archivo en el directorio webtest/tests llamado UsuarioTest.groovy que ya contiene una plantilla de lo que podría ser un test funcional. Nosotros vamos a modificar esta plantilla para que conseguir nuestro objetivo.
El primer test funcional que vamos a crear tal y como comentábamos anteriormente será un test que compruebe las operaciones básicas que se pueden realizar sobre los usuarios, que son listar, crear, editar y eliminar usuarios. Este test ya ha sido generado automáticamente por Grails cuando ejecutamos el comando grails create-webtest Usuario, aunque sobre él tendremos que hacer algunos cambios.
class UsuarioTest extends grails.util.WebTest { // Unlike unit tests, functional tests are sometimes sequence dependent. // Methods starting with 'test' will be run automatically in alphabetical order. // If you require a specific sequence, prefix the method name (following 'test') with a sequence // e.g. test001UsuarioListNewDelete def testUsuarioListNewDelete() { invoke 'usuario' verifyText 'Home' verifyListSize 0 clickLink 'New Usuario' verifyText 'Create Usuario' clickButton 'Create' verifyText 'Show Usuario', description:'Detail page' clickLink 'List', description:'Back to list view' verifyListSize 1 group(description:'edit the one element') { showFirstElementDetails() clickButton 'Edit' verifyText 'Edit Usuario' clickButton 'Update' verifyText 'Show Usuario' clickLink 'List', description:'Back to list view' } verifyListSize 1 group(description:'delete the only element') { showFirstElementDetails() clickButton 'Delete' verifyXPath xpath: "//div[@class='message']", text: /.*Usuario.*deleted.*/, regex: true } verifyListSize 0 } String ROW_COUNT_XPATH = "count(//div[@class='list']//tbody/tr)" def verifyListSize(int size) { ant.group(description:"verify Usuario list view with $size row(s)") { verifyText 'Usuario List' verifyXPath xpath: ROW_COUNT_XPATH, text: size, description:"$size row(s) of data expected" } } def showFirstElementDetails() { clickLink '1', description:'go to detail view' } }
Echando un vistazo rápido al código generado, podemos intuir su funcionamiento. Hacer clic sobre enlaces (clickLink) o botones (clickButton), comprobar que un texto está en la página (verifyText) y comprobar el contenido encerrado en etiquetas del código HTML (verifyXPath) son algunos de los métodos en estos tests funcionales. Además, se ha creado un método (verifyListSize()) para comprobar cuantos usuarios aparecen en el listado.
Si intentamos pasar el nuevo test de funcionalidad con el comando grails run-webtest y repasamos el informe correspondiente, veremos como este test ha fallado. Asegúrate de que la aplicación no se esté ejecutando en este momento, porque webtest necesita del puerto 8080. Este informe nos indica cuando tests se han ejecutado correctamente e incluso nos da información de cuantos pasos han llegado a ejecutarse. En nuestro primer intento, el 100% de nuestros tests han fallado, esto es 1 de 1. Que nuestra aplicación haya fallado es algo normal puesto que Grails ha creado por nosotros una primera instancia del test funcional que posteriormente nosotros debemos modificar para adaptarlo a los cambios que hayamos hecho en nuestro código.
Echando un vistazo al informe, vemos como lo primero que nos indica es que la primera llamada al método verifyListSize() ha fallado y además nos indica que el valor que esperaba era 0 mientras que el encontrado ha sido el valor 5. Esto es debido a que los tests funcionales cuentan también con la información añadida en el archivo conf/BootStrap.groovy, con lo que el número de usuarios al arrancar la aplicación empieza desde 5. Debemos cambiar todas las líneas en nuestro test donde se comprueba esto para aumentarle el valor en 5, por ejemplo la primera comprobación quedaría verifyListSize 5, mientras que la segunda una vez creado el usuario verifyListSize 6.
Si ahora volvemos a ejecutar el comando grails run-webtest veremos como el error anterior ya no se produce. Sin embargo, ahora tenemos otro problema y es que el test intenta crear un nuevo usuario sin haberse identificado en el sistema. Así que antes de empezar a crear usuarios debemos añadir el código necesario en el test para identificarnos en el sistema y posteriormente realizar todas las operaciones necesarias. Si añadimos el siguiente código inmediatamente después de verifyText 'Home' para identificarnos en el sistema, este problema estará solucionado.
group(description:'intento identificarme en el sistema') { showFirstElementDetails() clickLink 'Login' verifyText 'Login' clickButton 'Login' verifyText 'Logout' clickLink 'Home', description:'Back home' verifyText 'UsuarioController' clickLink 'UsuarioController' }
Si volvemos a realizar los tests funcionales veremos como ahora el problema que nos da es que el usuario no ha podido ser creado correctamente. Esto es debido a que hemos dejado en blanco todos los campos del formulario. Para rellenarlos podemos utilizar la función setInputField() de la siguiente forma:
setInputField(name:'login','usuario2') setInputField(name:'password','mipassword') setInputField(name:'nombre','Usuario') setInputField(name:'apellidos','Dos')
Ahora los campos necesarios para crear un usuario ya están completos y el test funcionará correctamente. Este será el contenido final del test funcional que acabamos de pasar satisfactoriamente.
class UsuarioTest extends grails.util.WebTest { // Unlike unit tests, functional tests are sometimes sequence dependent. // Methods starting with 'test' will be run automatically in alphabetical order. // If you require a specific sequence, prefix the method name (following 'test') with a sequence // e.g. test001UsuarioListNewDelete def testUsuarioListNewDelete() { invoke 'usuario' verifyText 'Home' group(description:'intento identificarme en el sistema') { showFirstElementDetails() clickLink 'Login' verifyText 'Login' clickButton 'Login' verifyText 'Logout' clickLink 'Home', description:'Back home' verifyText 'UsuarioController' clickLink 'UsuarioController' } verifyListSize 5 clickLink 'New Usuario' verifyText 'Create Usuario' setInputField(name:'login','usuario2') setInputField(name:'password','mipassword') setInputField(name:'nombre','Usuario') setInputField(name:'apellidos','Dos') clickButton 'Create' verifyText 'Show Usuario', description:'Detail page' clickLink 'List', description:'Back to list view' verifyListSize 6 group(description:'edit the one element') { showFirstElementDetails() clickButton 'Edit' verifyText 'Edit Usuario' clickButton 'Update' verifyText 'Show Usuario' clickLink 'List', description:'Back to list view' } verifyListSize 6 group(description:'delete the only element') { showFirstElementDetails() clickButton 'Delete' verifyXPath xpath: "//div[@class='message']", text: /.*Usuario.*deleted.*/, regex: true } verifyListSize 5 } String ROW_COUNT_XPATH = "count(//div[@class='list']//tbody/tr)" def verifyListSize(int size) { ant.group(description:"verify Usuario list view with $size row(s)") { verifyText 'Usuario List' verifyXPath xpath: ROW_COUNT_XPATH, text: size, description:"$size row(s) of data expected" } } def showFirstElementDetails() { clickLink '1', description:'go to detail view' } }
Un aspecto importante en los tests funcionales es el orden en el que se ejecutan. Los tests se ejecutan por orden alfabético, con lo que si en algún momento es importante el orden para que los tests se ejecuten correctamente debemos añadir al principio del nombre del test, una numeración del estilo 0001, 0002, etc, para que los tests se ejecuten en el orden que nosotros queremos.
Validación y errores
Si navegamos un poco por los ejemplos que hemos desarrollado a lo largo de las sesiones de Grails, podemos encontrar varias referencias a los errores en funciones como hasErrors() que son utilizados en los controladores para detectar la presencia de errores, así como de los mensajes flash para mostrar al usuario determinados mensajes tales Usuario creado o Usuario modificado. Veamos algunos ejemplos.
Vamos a probar a crear un nuevo usuario y olvidaremos indicarle todos los datos posibles (login, password, nombre y apellidos). Como imaginamos, la aplicación nos indicará que se han cometido una serie de errores y que ha sido imposible crear el nuevo usuario. Es más, Grails nos mostrará un listado con todos los errores que se han cometido en el formulario. Si ahora abrimos el archivo de la vista create.gsp, rápidamente veremos el fragmento de código que se encarga de esta gestión de errores.
<g:hasErrors bean="${usuarioInstance}"> <div class="errors"> <g:renderErrors bean="${usuarioInstance}" as="list" /> </div> </g:hasErrors>
En primer lugar, la etiqueta <g:hasErrors> comprueba si se han producido errores en el bean pasado por parámetro. En caso afirmativo, éstos se imprimirán gracias a la etiqueta <g:renderErrors>, la cual recorre todos los errores mostrándolos uno por uno. Esta última etiqueta permite el parámetro field para el caso de que sólo queramos mostrar el error producido en un determinado campo. Por ejemplo, si sólo queremos mostrar el error producido en el campo login podríamos escribir <g:renderErrors bean="${book}" as="list" field="title"/>. Lo mismo sucede con la etiqueta <g:hasErrors>.
Sin embargo, esta técnica funciona porque la página se genera directamente desde el controlador, algo que no siempre sucede. Por ejemplo, en el mismo caso de crear un nuevo usuario, si todo ha ido correctamente el usuario es redirigido a la vista show.gsp para mostrar los datos del usuario recién creado, pero además, en la parte superior se muestra un texto indicando que el usuario ha sido creado correctamente. Recordemos que el código de la función save() es el siguiente:
def save = { def usuarioInstance = new Usuario(params) if(!usuarioInstance.hasErrors() && usuarioInstance.save()) { flash.message = "Usuario ${usuarioInstance.id} created" redirect(action:show,id:usuarioInstance.id) } else { render(view:'create',model:[usuarioInstance:usuarioInstance]) } }
Como vemos, si no se ha producido ningún error y el usuario se ha podido almacenar en la base de datos, se almacena en la variable flash.message el texto que deseamos mostrar en la parte superior y posteriormente se redirige al usuario a la vista show.gsp. Si ahora abrimos el archivo de la vista show.gsp podemos comprobar que antes de mostrar los datos del usuario seleccionado, se comprueba que la variable flash.message no esté vacía y en caso afirmativo se mostrará el contenido de esa variable.
<g:if test="${flash.message}"> <div class="message">${flash.message}</div> </g:if>
En los casos en los que se utiliza la redirección entre páginas es donde Grails hace uso de la técnica de los mensajes flash. Posiblemente habrían otras soluciones como la posibilidad de almacenar en la variable session este tipo de mensajes y comprobarlos en cada página. No obstante, esto supone tener que borrar el contenido de esta variable para no mostrarlo por duplicado, algo que en cualquier momento se nos puede pasar por alto.
Y es ahí donde Grails hace el trabajo por nosotros, ya que su solución es en parte el caso que acabamos de exponer. Grails utiliza el ámbito flash al igual que existen otros ámbitos como application, session, request y page, tal y como comentábamos en la sesión anterior. El nuevo ámbito flash permite almacenar variables en forma de mapa que pueden ser accedidas en cualquier momento. Pero lo mejor de este ámbito es que su contenido se destruye entre peticiones, con lo que el problema de olvidarnos de eliminar el mensaje está solucionado.
Externalización de cadenas de caracteres
Hasta ahora, casi todos los mensajes de nuestra aplicación están directamente incluidos tanto en el código de los controladores como en el de las vistas. Por ejemplo, cuando editamos un usuario y se realiza con éxito, tenemos que el método update() del controlador de la clase Usuario almacena una variable en el ámbito flash con el mensaje a mostrar al usuario de la siguiente forma flash.message = "Usuario ${params.id} updated". Esto mismo sucede en las vistas, donde directamente aparecen mensajes o texto que se muestran posteriormente en nuestras páginas.
Imaginemos, que al cliente que va a utilizar la aplicación no le gustan determinados mensajes mostrados y los quiere modificar. En ese caso, deberíamos editar todos los archivos implicados en los cambios solicitados con la correspondiente pérdida de tiempo.
Otro problema que se plantea al actuar de esta forma es la imposibilidad de internacionalizar la aplicación. La internacionalización de una aplicación, entre otras cosas, supone la necesidad de disponer de ficheros de traducción externos con los mensajes utilizados en ella (entre otras cosas).
En este sentido y para mejorar esta característica, Grails pone a nuestra disposición el directorio grails-app/i18n/ para gestionar estos ficheros de traducción de cadenas de texto. Si echamos un vistazo a este directorio, veremos que existen una serie de ejemplos ya generados para varios de los idiomas más comunes (inglés, español, francés, alemán, etc.). Si abrimos por ejemplo el archivo message.properties veremos determinadas cadenas de texto que nos resultarán familiares, ya que son las que se muestran en nuestra aplicación cuando se produce un error en la misma.
.... default.doesnt.match.message=Property [{0}] of class [{1}] with value [{2}] does not match the required pattern [{3}] default.invalid.url.message=Property [{0}] of class [{1}] with value [{2}] is not a valid URL default.invalid.creditCard.message=Property [{0}] of class [{1}] with value [{2}] is not a valid credit card number default.invalid.email.message=Property [{0}] of class [{1}] with value [{2}] is not a valid e-mail address default.invalid.range.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid range from [{3}] to [{4}] ....
Para empezar, vamos a crear un par de nuevas entradas en este fichero que contendrán las cadenas de texto referentes a Login y Logout. Para ello, al final del archivo message.properties vamos a añadir las siguientes líneas:
encabezado.login = Login encabezado.logout = Logout
El siguiente paso será modificar la página _header.gsp para contemplar la nueva metodología. Os recuerdo que está página se encuentra en el directorio common. A partir de ahora, en lugar de escribir directamente el texto que queremos mostrar en nuestra página gsp, vamos a utilizar la etiqueta <g:message> pasándole el parámetro code en el que le especificaremos el nombre de la nueva cadena de traducción añadida al archivo message.properties. El archivo _header.gsp quedaría así:
<div id="menu"> <nobr> <g:if test="${session.usuario}"> <b>${session.usuario?.nombre} ${session.usuario?.apellidos}</b> | <g:link controller="usuario" action="logout"><g:message code="encabezado.logout"/></g:link> </g:if> <g:else> <g:link controller="usuario" action="login"><g:message code="encabezado.login"/></g:link> </g:else> </nobr> </div>
El archivo que nosotros hemos modificado es el que Grails utiliza por defecto, sin embargo, Grails intenta resolver la localización del usuario para mostrarle los mensajes en el idioma correcto. Así que, es más que probable Grails esté mostrando los mensajes de error de la aplicación en castellano y no en inglés, que es el archivo que hemos modificado. Esto dependerá de como tengamos configurado las opciones de idiomas de nuestro navegador.
El siguiente paso va a ser añadir estas dos nuevas variables en el archivo message_es.properties que quedará de la siguiente forma:
encabezado.login = Identificarse encabezado.logout = Salir
Esto que acabamos de hacer es un primer paso hacia la internacionalización de nuestra aplicación. El principal problema de internacionalizar nuestra aplicación se refiere a los tests funcionales que veíamos anteriormente. Estos tests comprueban la existen de determinados textos en las páginas de la aplicación para testear su validez y si ahora cambiamos estos textos, es posible que nos encontremos con que un test funcional que antes se ejecutaba correctamente ahora ha dejado de funcionar.
Pero vayamos más allá de mostrar un simple mensaje de texto. ¿Qué pasa si queremos mostrar un mensaje personalizado del estilo El usuario Pablo Mar Mol ha sido modificado correctamente? Para poder hacer esto, necesitamos pasar parámetros al mensaje a mostrar, lo cual debemos hacer en tres partes. En primer lugar, especificando en la cadena de texto del archivo message.properties los parámetros y la posición de éste en la cadena, como por ejemplo usuario.updated.message = El usuario {0} {1} ha sido modificado correctamente, donde los valores {0} y {1} serán sustituidos posteriormente por el nombre y apellidos del usuario. Será necesario tener esto en cuenta a la hora de definir las cadenas de texto.
En segundo lugar, debemos modificar las controladores para que en lugar de mostrar un mensaje de texto directamente, éste acepte la posibilidad de tener parámetros. Por ejemplo, en el fragmento de código donde se modifica la información del usuario (update()) tenemos el siguiente código
if(!usuarioInstance.hasErrors() && usuarioInstance.save()) { flash.message = "Usuario ${params.id} updated" redirect(action:show,id:usuarioInstance.id) }
Para la externalización de esta cadena, necesitamos pasar una serie de argumentos y aprovecharemos también para pasar un texto por defecto para el caso en que el código de la cadena no se encuentre en el archivo message.properties. Podemos sustituir el código anterior por el siguiente:
if(!usuarioInstance.hasErrors() && usuarioInstance.save()) { flash.message = "usuario.updated.message" flash.args = [usuarioInstance.nombre, usuarioInstance.apellidos] flash.defaultMsg = "Usuario modificado correctamente" redirect(action:show,id:usuarioInstance.id) }
Si comprobamos ahora el mensaje que nos muestra la aplicación al editar los datos de un usuario, comprobaremos como el mensaje mostrado es usuario.updated.message, que no es lo que queremos. Esto es debido a que en la vista correspondiente donde se muestra el resultado de la operación de editar un usuario, show.gsp seguimos teniendo el mismo código de antes y no hemos realizado ninguna modificación, tal y como se ve en el siguiente fragmento de código.
<g:if test="${flash.message}"> <div class="message">${flash.message}</div> </g:if>
Si queremos aplicar los cambios de los que hemos hablado, ya no podemos mostrar directamente el contenido de la variable flash.message, porque ésta ha dejado de contener el mensaje a mostrar y ahora su contenido es el nombre de una variable del archivo message.properties. Para esto, necesitamos utilizar la etiqueta <g:message> con una serie de parámetros, tal y como aparece en el siguiente trozo de código.
<div class="message"> <g:message code="${flash.message}" args="${flash.args}" default="${flash.defaultMsg}"/> </div>
Ahora sí, nuestro mensaje aparece tal y como nosotros queremos.