Java: el paso de parámetros es por valor

El post de hoy es para resolver una duda sobre Java básico que a veces surge incluso entre gente con mucha experiencia: ¿Cómo es el paso de parámetros en Java? ¿Es por valor o por referencia?

Dependiendo del lenguaje de programación, el paso de parámetros puede ser por valor o por referencia:

  • El paso por valor es cuando al pasar una variable como parámetro a un método, el valor de ese parámetro se copia en una variable «temporal» que solo estará vigente durante la ejecución de ese método. De manera que al salir del método, la variable inicial tendrá el mismo valor que al inicio, ya que dentro del método no puede modificarse.
  • En el paso por referencia, en cambio, al pasar una variable como parámetro a un método, se pasa una referencia a esa variable. Con lo que si dentro del método es modificada, al salir del método el valor de la variable será distinto al inicial.

En Java el paso de parámetros es siempre por valor. Siempre. Varias veces he oído a gente decir que el paso en Java es por referencia, o, más concrétamente, que el paso es por valor si son datos primitivos y por referencia si son objetos. Esto último lo cree mucha gente, y no es cierto realmente, aunque lo pueda parecer.

Vamos a explicarlo con un pequeño ejemplo, veamos el siguiente código:

    public void primitiveExample() {
        int years = 10;
        System.out.println("Years before method: " + years);
        methodWithPrimitive(years);
        System.out.println("Years after method: " + years);
    }

    private void methodWithPrimitive(int arg) {
        arg = arg + 3;
        System.out.println("Parameter modified: " + arg);
    }

En el código pasamos un int ‘years’ como parámetro a un método y dentro del método le asignamos un nuevo valor.
Si ejecutamos ese código, la salida sería la siguiente:

Years before method: 10
Parameter modified: 13
Years after method: 10

Como vemos, al salir del método la variable ‘years’ tiene el mismo valor que tenia antes de entrar en el método, aún habiéndose modificado en él. Así funciona el paso por valor.

Veamos ahora el siguiente ejemplo, con un objeto en lugar de un tipo primitivo:

    class User {
        private String name;

        public User(String name) {
            this.name = name;
        }

        //getter and setter...

        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    ...
    public void objectExample() {
        User user = new User("edwise");
        System.out.println("User before method: " + user);
        methodWithObjectSet(user);
        System.out.println("User after method: " + user);
    }

    private void methodWithObjectSet(User arg) {
        arg.setName("newUser");
        System.out.println("Parameter modified: " + arg);
    }

Aquí, creamos un ‘User’ (clase muy simple con un atributo String), se lo pasamos como parámetro a un método y dentro de este llamamos a un método set para modificar un atributo de nuestro objeto.
La salida por consola sería la siguiente:

User before method: User{name='edwise'}
Parameter modified: User{name='newUser'}
User after method: User{name='newUser'}

Si, ahora, como vemos, nuestro parámetro ha sido modificado dentro del método, y esa modificación perdura en nuestro objeto. ¿Es entonces esto paso por referencia? No… realmente es paso por valor, como siempre. ¿Qué ha pasado entonces?

Veamos un último ejemplo, y lo explicamos:

    public void otherObjectExample() {
        User user = new User("edwise");
        System.out.println("User before method: " + user);
        methodWithObjectNew(user);
        System.out.println("User after method: " + user);
    }

    private void methodWithObjectNew(User arg) {
        arg = new User("newUser");
        System.out.println("Parameter modified: " + arg);
    }

En este caso hacemos casi lo mismo que en el código anterior. Dentro del código estamos modificando nuestro objeto, pero ahora creando un nuevo y asignandoselo. El resultado debería ser el mismo.
Salida:

User before method: User{name='edwise'}
Parameter modified: User{name='newUser'}
User after method: User{name='edwise'}

No, en este caso nuestro objeto no se ha modificado. Aquí si parece claro que el paso es por valor.

Para entender el porque de nuestro segundo ejemplo, la clave es recordar que, en Java, todas las variables que «son» objetos realmente son referencias a objetos, no son el objeto en si. De manera que, tanto en nuestro segundo y tercer ejemplo al método le pasamos una referencia a un objeto. Y como el paso es por valor, se crea una variable temporal para el método en la que se copia el valor de la variable que le pasamos, que es una referencia. Luego entonces, dentro del método tenemos una nueva referencia que apunta al mismo objeto.

En el segundo ejemplo (método ‘objectExample‘), llamamos desde nuestra nueva referencia (‘arg’) al método set del objeto. Como el objeto sobre el que llamamos a ese método es el mismo que para la referencia de fuera, nuestro objeto inicial es modificado.

En el tercer ejemplo (método ‘otherObjectExample‘), lo que realmente estamos haciendo es crear un nuevo objeto, y que la referencia ‘arg’ apunte/referencie a ese objeto. Al salir del método la referencia ‘arg’ desaparece, y la ‘user’ sigue apuntando al primer objeto, el cual no ha sido modificado, claro. (Además, el objeto creado dentro del método será destruido por el Gargabe Collector al no tener ninguna referencia que apunte a él, pero eso ya es otra historia).

Conclusiones

El paso de parámetros en Java es siempre por valor. Pero al ser referencias a objetos las «variables objetos», desde dentro de un método podemos acceder y modificar el objeto original. Y hay que tener mucho cuidado cuando asignemos un nuevo objeto a un parámetro de un método, ya que el resultado no va a ser el esperado…

De todas maneras, asignar nuevos valores / objetos a un parámetro de un método no suele considerarse una buena práctica, con lo que si hacemos un código medianamente limpio no deberíamos tener estos problemas…

Tenéis en mi github (proyecto passbyvalue-example) el código completo de este post, para poder ejecutarlo y comprobar que es cierto todo esto 😉

Tests de integración para un servicio REST, con Spring

En mis dos últimos post he explicado como configurar los tests de integración con Spring Boot y como configurarlos además con Maven. Teniendo ya toda esa configuración, es hora ya de implementar unos tests de integración completos, para un servicio REST.

Concepto:
Ya expliqué en los anteriores post el concepto de tests de integración: en resumen, son tests en los que probamos varios componentes de un sistema trabajando juntos (a diferencia de los unitarios, en los que solo probamos cada componente por separado).
En este ejemplo voy a realizar unos tests de integración completos de un servicio REST (el mismo usado en los últimos posts, un servicio REST para un recurso sencillo ‘Info’). Para realizar los tests correctamente usaré, por un lado el clásico MockMvc de Spring, y para probar los jsons que devuelve el rest usaré la librería jsonpath.

Entorno usado:
Java JDK 1.8
Maven 3.2.1
Git 1.9.5
IDE Intellij 14.0.3 Ultimate version

Pasos:

1. Los tests van a tratar simplemente de probar todo el servicio REST Info el cual tenemos implementado en InfoController:

package com.edwise.pocs.integrationtestsrest.controller;

// imports...

@RestController
@RequestMapping("/api/info/")
public class InfoController {

    @Autowired
    private InfoService infoService;

    @ResponseStatus(HttpStatus.OK)
    @RequestMapping(method = RequestMethod.GET,
            produces = MediaType.APPLICATION_JSON_VALUE)
    public List<Info> getAllInfos() {
        return infoService.findAll();
    }

    @ResponseStatus(HttpStatus.OK)
    @RequestMapping(method = RequestMethod.GET, value = "{id}",
            produces = MediaType.APPLICATION_JSON_VALUE)
    public Info getInfo(@PathVariable Long id) {
        return infoService.findOne(id);
    }

    @RequestMapping(method = RequestMethod.POST,
            produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Info> createInfo(@RequestBody Info info) {
        Info infoCreated = infoService.save(info);
        return new ResponseEntity<>(infoCreated, HttpStatus.CREATED);
    }

    @ResponseStatus(HttpStatus.NO_CONTENT)
    @RequestMapping(method = RequestMethod.PUT, value = "{id}")
    public void updateInfo(@PathVariable Long id, @RequestBody Info info) {
        infoService.update(info.setId(id));
    }

    @ResponseStatus(HttpStatus.NO_CONTENT)
    @RequestMapping(method = RequestMethod.DELETE, value = "{id}")
    public void deleteInfo(@PathVariable Long id) {
        infoService.delete(id);
    }

}

Como se puede ver, he completado el controller de los posts anteriores con un método PUT (para actualizar) y un método GET all, para tener lo que podría ser un servicio REST completo.

2. Añadimos a las dependencias de maven la librería jsonpath, para testear fácilmente jsons:

     ...
     <dependencies>
          ...
         <dependency>
             <groupId>com.jayway.jsonpath</groupId>
             <artifactId>json-path-assert</artifactId>
             <version>1.2.0</version>
             <scope>test</scope>
         </dependency>
     </dependencies>
     ...

2. Comenzamos nuestros tests de integración con la configuración que hicimos en los posts anteriores, tanto de maven como de Spring y Spring Boot. Para ello usaremos la clase que ya creamos, InfoControllerIT:

package com.edwise.pocs.integrationtestsrest.controller;

// imports...

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {Application.class})
@WebIntegrationTest({"server.port=0"})
public class InfoControllerIT {

    @Autowired
    protected WebApplicationContext webApplicationContext;

    private MockMvc mockMvc;

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders
                .webAppContextSetup(this.webApplicationContext)
                .build();
    }

    // TODO aquí implementaremos los tests
}

3. Creamos nuestro primer test. En el vamos a probar el método GET /api/info/{id}, es decir, el que obtiene un objeto Info por su id. Lo implementaremos así:

    @Test
    public void getInfo_InfoFound_ShouldReturnCorrectInfo() throws Exception {
        mockMvc.perform(get("/api/info/{id}", INFO_ID_1234))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$.id", is(INFO_ID_1234.intValue())))
                .andExpect(jsonPath("$.infoText", is(INFO_TEXT_1234)))
                .andExpect(jsonPath("$.creationDateTime", is(notNullValue())));
    }

Todos nuestros tests de integración consisten en una llamada con el objeto MockMvc. En ella se ve como realizamos un get, con un parámetro de path variable (id), y sobre esa llamada hacemos varías comprobaciones (métodos ‘andExpect‘):
– Que el http status devuelto es OK (200).
– Que el contenido recibido es de tipo JSON.
– Usando la librería jsonpath comprobamos que el body de la respuesta contiene un json correcto (en este caso compruebo que el id y el texto son los correctos y que la fecha no es nula, por ejemplo).

4. Implementamos el test que pruebe el método que devuelve todos los Info’s, GET /api/info/:

    @Test
    public void getAll_InfosFound_ShouldReturnFoundInfos() throws Exception {
        mockMvc.perform(get("/api/info/"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$", hasSize(3)))
                .andExpect(jsonPath("$[0].id", is(INFO_ID_120.intValue())))
                .andExpect(jsonPath("$[0].infoText", is(INFO_TEXT_1234)))
                .andExpect(jsonPath("$[0].creationDateTime", is(notNullValue())))
                .andExpect(jsonPath("$[1].id", is(INFO_ID_121.intValue())))
                .andExpect(jsonPath("$[1].infoText", is(INFO_TEXT_4567)))
                .andExpect(jsonPath("$[1].creationDateTime", is(notNullValue())))
                .andExpect(jsonPath("$[2].id", is(INFO_ID_122.intValue())))
                .andExpect(jsonPath("$[2].infoText", is(INFO_TEXT_7892)))
                .andExpect(jsonPath("$[2].creationDateTime", is(notNullValue())));
    }

El tests es muy similar al anterior, lo único que en este caso nos devuelve un array de Info’s, con lo que comprobamos el tamaño del array (método hasSize), y cada uno de los Info’s por separado, con jsonpath, accediendo a cada Info como si fuera un array.

5. Seguimos con el test que pruebe el método crea un nuevo Info, POST /api/info/, y el que actualice un Info ya existente, PUT /api/info/{id}:

    @Test
    public void postInfo_InfoCorrect_ShouldReturnCreatedStatusAndCorrectInfo() throws Exception {
        mockMvc.perform(post("/api/info/")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"infoText\":\"Info 1234 New\",\"creationDateTime\":\"2013-10-11T20:10:10\"}"))
                .andExpect(status().isCreated())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$.id", is(INFO_ID_1234.intValue())))
                .andExpect(jsonPath("$.infoText", is(INFO_TEXT_1234_NEW)))
                .andExpect(jsonPath("$.creationDateTime", is(notNullValue())));
    }

    @Test
    public void putInfo_InfoCorrect_ShouldReturnNoContentStatus() throws Exception {
        mockMvc.perform(put("/api/info/{id}", INFO_ID_1234)
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"infoText\":\"Info 1234 Updated\",\"creationDateTime\":\"2014-10-25T19:16:21\"}"))
                .andExpect(status().isNoContent());
    }

En el primero, realizamos un POST y le marcamos como ‘Content-Type’ del mismo el típo ‘application/json’. Luego le pasamos como contenido de la request el json del objeto a crear. Las comprobaciones son las de siempre, comprobando en este caso que el http status es CREATED (201).
En el segundo, realizamos un PUT, con path variable (el id de siempre) y hacemos lo mismo que en el POST, excepto que no comprobamos el json del body (no devuelve nada) y que el http status esperado es NO CONTENT (204).

6. Por último, implementamos el que pruebe el método que borre un Info, DELETE /api/info/{id}:

    @Test
    public void deleteInfo_ShouldReturnNoContentStatus() throws Exception {
        mockMvc.perform(delete("/api/info/{id}", INFO_ID_1234))
                .andExpect(status().isNoContent());
    }

Realizamos un DELETE, con el id path variable, y solo es necesario comprobar el http status, NO CONTENT (204).

7. Y hemos terminado. Nuestra clase completa ‘InfoControllerIT’ quedaría así. Podemos ahora ejecutar los tests tanto desde Intellij como con maven (profile ‘integration-test’, con el goal ‘verify’).

8. Temas importantes a tener en cuenta:
– El servicio REST implementado es muy sencillo y tiene la capa de datos mockeada. Si tuviéramos un servicio completo, con su capa de base de datos, habría que buscar como configurar una base de datos en memoria o similar, para realizar los tests (o incluso mockear con mockito la capa de datos).
– Faltarían varios tests, principalmente tests que fallen: GET, PUT y DELETE sobre un Info que no exista, etc.
– El servicio REST, al realizar un POST, devuelve en el body de la response el json completo. Realmente, lo más correcto sería no devolver nada en el body, y devolver en el ‘location’ de los headers un link al nuevo Info creado (principio HATEOAS). Esto probablemente lo veamos en otro post más adelante.

Si queréis descargaros el proyecto completo, lo tenéis como siempre en mi github (proyecto integrationtests-rest-example).