Search code examples
javaspringjpatestngh2

Spring and TestNG @DataJpaTest does not run AttributeConverters for @Convert annotated attributes


I have a very basic AttributeConverter implementation that I want to test. As such, I'll first apply a simple hash, but instead of un-hashing I'll just throw an UnsupportedOperationException.

public class EncryptorConverter implements AttributeConverter<String, byte[]> {
    @Override
    public byte[] convertToDatabaseColumn(String attribute) {
        // Apply hash
    }

    @Override
    public String convertToEntityAttribute(byte[] dbData) {
        throw new UnsupportedOperationException();
    }
}

And an entity

@Entity
public class Account {
  @Id
  private String id;
  @Column
  @Convert(converter = EncryptorConverter.class)
  private String sensitiveInfo;
}

And let's say I have a very fundamental repository for Account:

@Repository
public interface AccountRepository extends JpaRepository<Account, String> {
}

I want to make sure that my EncryptorConverter is being called in a @DataJpaTest, like this:

@DataJpaTest(excludeAutoConfiguration = {LiquibaseAutoConfiguration.class})
public class AccountRepositoryTest extends AbstractTransactionalTestNGSpringContextTests {
  @Autowired
  private AccountRepository accountRepository;

  public void shouldSaveEncryptedData() throws Exception {
    Account expectedAccount = new Account("1", "sensitive info");
    accountRepository.save(expectedAccount);

    // I was expecting an exceptioon with UnsupportedOperationException as root cause here
    Optional<Account> actualAccount = accountRepository.findById("1");

    assertTrue(actualAccount.isPresent());
    assertEquals(actualAccount.get(), expectedAccount);
  }
}

I expected the aforementioned test to fail, but instead it passes. All debug I made suggested that the entity was persisted without any encryption in the DB. I already read this article, which seemed more promising for the problem I'm facing, but it didn't help much.

Does someone know what to do here?


Extra: this is how I'm configuring my test DB:

@EnableJpaRepositories(basePackages = {"..."})
@EnableTransactionManagement
public class TestDatabaseConfig {

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, JpaVendorAdapter jpaVendorAdapter) {
        LocalContainerEntityManagerFactoryBean lef = new LocalContainerEntityManagerFactoryBean();
        lef.setDataSource(dataSource);
        lef.setJpaVendorAdapter(jpaVendorAdapter);
        lef.setPackagesToScan("...");
        return lef;
    }

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .build();
    }

    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(true);
        hibernateJpaVendorAdapter.setGenerateDdl(true);
        hibernateJpaVendorAdapter.setDatabase(Database.H2);
        return hibernateJpaVendorAdapter;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        return new JpaTransactionManager();
    }
}

Solution

  • The problem isn't the fact that the converter isn't being picked up, the problem is your test and lack of understanding of how Hibernate works.

    You are saving the entity, which puts it in the first level cache, next you do a findById which will result in an EntityManager.find which will first look into the 1st level cache and as the entity is found no query nor conversion will take place.

    To fix, simulate a transaction. For this inject the TestEntityManager into your class and flush and clear after the save. This will issue the SQL to the database and clear the 1st level cache.

    @DataJpaTest(excludeAutoConfiguration = {LiquibaseAutoConfiguration.class})
    public class AccountRepositoryTest extends AbstractTransactionalTestNGSpringContextTests {
      
      @Autowired
      private TestEntityManager tem;
    
      @Autowired
      private AccountRepository accountRepository;
    
      @Test
      public void shouldSaveEncryptedData() throws Exception {
        Account expectedAccount = new Account("1", "sensitive info");
        accountRepository.save(expectedAccount);
        
        // Simulate commit/tx end
        tem.flush();
        tem.clear();
    
        Optional<Account> actualAccount = accountRepository.findById("1");
    
        assertTrue(actualAccount.isPresent());
        assertEquals(actualAccount.get(), expectedAccount);
      }
    }