grails-core icon indicating copy to clipboard operation
grails-core copied to clipboard

Mock Service | Grails 5.3.3

Open urmichm opened this issue 1 year ago • 10 comments
trafficstars

Expected Behavior

When running unit test on a controller

class MyControllerTest extends Specification implements ControllerUnitTest<MyController>, DataTest

I need to run unit tests for the controller

Actual Behaviour

Exception

NoSuchBeanDefinitionException: No qualifying bean of type 'absolutely.unrelated.Service'

Steps To Reproduce

  1. Grails 5.3.3
  2. Multiple contollers and services
"org.grails:grails-web-testing-support:3.1.2"
"org.grails:grails-gorm-testing-support:3.1.2"

Environment Information

Ububntu java 1.8

Example Application

No response

Version

5.3.3

urmichm avatar May 06 '24 07:05 urmichm

Similar issue is about DataSource.

NoSuchBeanDefinitionException: No qualifying bean of type 'javax.sql.DataSource' available: expected at least 1 bean which qualifies as autowire candidate.

Data source is a nested bean far down the hierarchy of beans. Running a Unit Test, i am just expecting a UNIT peace of code to be tested. No need for data source or loading all the beans of the application.

urmichm avatar May 06 '24 09:05 urmichm

@urmichm Does the test at https://github.com/osscontributor/issue13506/blob/25d620dd85b666ba5c0a3b9ad194dcabc739ac63/src/test/groovy/issue13506/MyControllerSpec.groovy pass for you?

jeffscottbrown avatar May 08 '24 19:05 jeffscottbrown

Hello, unfortunately no. I am getting an exceptions during the initializationError that some other Beans are not created. example: NoSuchBeanDefinitionException: No qualifying bean of type 'javax.sql.DataSource' available: expected at least 1 bean which qualifies as autowire candidate. The DataSource is required in an unrelated Service which is in some other unrelated Controller.

urmichm avatar May 10 '24 06:05 urmichm

Hello, unfortunately no.

That is interesting.

Are you using Gradle to run the test?

This is what I am seeing: https://gist.github.com/osscontributor/1786f4cce7e73aa943f3f6fc479fc4b4

jeffscottbrown avatar May 10 '24 18:05 jeffscottbrown

The DataSource is required in an unrelated Service which is in some other unrelated Controller.

I have tried a few combinations of class dependencies and I haven't yet been able to reproduce the problem. If you could send a PR to the repo I linked above with a combination of things that recreates the problem, that would prove helpful in troubleshooting.

jeffscottbrown avatar May 15 '24 13:05 jeffscottbrown

Maybe let me paraphrase the question. @osscontributor how do you mock or configure DataSource for unit tests?

urmichm avatar May 16 '24 14:05 urmichm

how do you mock or configure DataSource for unit tests?

If you are using GORM, you generally wouldn't. The more common thing is to kind of mock GORM using the default implementation that is configured in unit tests which uses a Map backed store. It is unusual for a controller to interact directly with a DataSource.

If you can provide a sample app which demonstrates what you are trying to test I would be happy to send you a PR or identify if there is relevant bug in the framework.

Thank you for the feedback.

jeffscottbrown avatar May 17 '24 12:05 jeffscottbrown

Hello @osscontributor The controller does not interact with a DataSource. The other controller has a service which has a bean which has a DataSource bean. I do not understand why the other controller and all its hierarchy of the beans is created. I do not need it. I just need a unit test for MyController. My question is about mocking the beans i do not need. Especially mocking a DataSource bean from a random Bean of a random Service from an unrelated Controller. I the unit tests I want to make the DataSource is not required at all, since as you mentioned DataSource is not there (at least directly)

urmichm avatar May 22 '24 06:05 urmichm

@urmichm Can you share a link to a sample project which demonstrates the problem?

jeffscottbrown avatar May 28 '24 15:05 jeffscottbrown

I assume that it might be that you are calling another controller from the one under unit test. But, if could share the code which is unit tested here or better a sample application that'd be helpful to debug the issue.

Generally, you would mock any service which is being called from the controller. Let's walk through an example:

  1. Suppose you have a BookController.groovy which inject a service named BookService as:

    grails-app/controllers/com/example/BookController.groovy

    
    package com.example
    
    import grails.rest.RestfulController
    
    class BookController extends RestfulController<Book> {
    
        BookService bookService
    
        BookController() {
            super(Book)
        }
    
        def show(Long id) {
            def book = bookService.getBookById(id)
            if (book) {
                respond book
            } else {
                render status: 404
            }
        }
    }
    
  2. Here is the code from BookService:

    grails-app/services/com/example/BookService.groovy

    package com.example
    
    import groovy.sql.Sql
    import javax.sql.DataSource
    
    class BookService {
    
        DataSource dataSource
    
        Book getBookById(Long id) {
            Sql sql = new Sql(dataSource)
            def row = sql.firstRow('SELECT * FROM book WHERE id = ?', [id])
            if (row) {
                new Book(row)
            } else {
                null
            }
        }
    }
    
  3. Ensure that the DataSource is properly configured in application.yml or application.groovy. Grails typically auto-configures the DataSource based on the environment.

    grails-app/config/application.yml

    dataSource:
        pooled: true
        driverClassName: org.h2.Driver
        username: sa
        password: ''
        dbCreate: update
        url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
    
    environments:
        development:
            dataSource:
                dbCreate: update
                url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
        test:
            dataSource:
                dbCreate: update
                url: jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
        production:
            dataSource:
                dbCreate: update
                url: jdbc:h2:file:prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
    
  4. When you write the unit tests for BookController.groovy then you might mock the BookService and test controller as:

    package com.example
    
    import grails.testing.web.controllers.ControllerUnitTest
    import spock.lang.Specification
    
    class BookControllerSpec extends Specification implements ControllerUnitTest<BookController> {
    
        def bookService = Mock(BookService)
    
        void setup() {
            controller.bookService = bookService
        }
    
        void "test show action with existing book"() {
            given:
            def book = new Book(title: "Grails in Action")
            bookService.getBookById(1L) >> book
    
            when:
            controller.show(1L)
    
            then:
            response.status == 200
            response.json.title == "Grails in Action"
        }
    
        void "test show action with non-existing book"() {
            given:
            bookService.getBookById(1L) >> null
    
            when:
            controller.show(1L)
    
            then:
            response.status == 404
        }
    }
    
  5. When writing a unit test for BookService, you might mock the DataSource and test the service logic.

    package com.example
    
    import grails.testing.services.ServiceUnitTest
    import spock.lang.Specification
    import groovy.sql.Sql
    import javax.sql.DataSource
    
    class BookServiceSpec extends Specification implements ServiceUnitTest<BookService> {
    
        def dataSource = Mock(DataSource)
        def sql = Mock(Sql)
    
        void setup() {
            service.dataSource = dataSource
        }
    
        void "test getBookById with existing book"() {
            given:
            def mockRow = [id: 1L, title: 'Grails in Action']
            sql.firstRow('SELECT * FROM book WHERE id = ?', [1L]) >> mockRow
    
            when:
            def book = service.getBookById(1L)
    
            then:
            1 * dataSource.connection >> Mock(Connection)
            1 * new Sql(connection) >> sql
            1 * sql.firstRow('SELECT * FROM book WHERE id = ?', [1L]) >> mockRow
            book.title == 'Grails in Action'
        }
    
        void "test getBookById with non-existing book"() {
            given:
            sql.firstRow('SELECT * FROM book WHERE id = ?', [1L]) >> null
    
            when:
            def book = service.getBookById(1L)
    
            then:
            1 * dataSource.connection >> Mock(Connection)
            1 * new Sql(connection) >> sql
            1 * sql.firstRow('SELECT * FROM book WHERE id = ?', [1L]) >> null
            book == null
        }
    }
    

Please note that above examples are only for reference. I didn't get the chance to verify this manually but would need to mock things as per the application and the specific services, methods, or contructors.

puneetbehl avatar May 28 '24 16:05 puneetbehl

I assume that it might be that you are calling another controller from the one under unit test.

I believe there is no good reason to do that in a unit test.

jeffscottbrown avatar Nov 07 '24 19:11 jeffscottbrown