2014-06-21 11 views
29

Sto utilizzando Spring/Spring-data-JPA e mi trovo a dover forzare manualmente un commit in un test di unità. Il mio caso d'uso è che sto facendo un test multi-thread in cui devo usare i dati che vengono mantenuti prima che i thread vengano spawnati.Come forzare manualmente un commit in un metodo @Transactional?

Sfortunatamente, poiché il test è in esecuzione in una transazione @Transactional, anche un flush non lo rende accessibile ai thread generati.

@Transactional 
    public void testAddAttachment() throws Exception{ 
     final Contract c1 = contractDOD.getNewTransientContract(15); 
     contractRepository.save(c1); 

     // Need to commit the saveContract here, but don't know how!     
     em.getTransaction().commit(); 

     List<Thread> threads = new ArrayList<>(); 
     for(int i = 0; i < 5; i++){ 
      final int threadNumber = i; 
      Thread t = new Thread(new Runnable() { 
       @Override 
       @Transactional 
       public void run() { 
        try { 
         // do stuff here with c1 

         // sleep to ensure that the thread is not finished before another thread catches up 
         Thread.sleep(1000); 
        } catch (InterruptedException e) { 
         // TODO Auto-generated catch block 
         e.printStackTrace(); 
        } 
       } 
      }); 
      threads.add(t); 
      t.start(); 
     } 

     // have to wait for all threads to complete 
     for(Thread t : threads) 
      t.join(); 

     // Need to validate test results. Need to be within a transaction here 
     Contract c2 = contractRepository.findOne(c1.getId()); 
    } 

Ho provato con il gestore di entità, ma un messaggio di errore quando lo faccio:

org.springframework.dao.InvalidDataAccessApiUsageException: Not allowed to create transaction on shared EntityManager - use Spring transactions or EJB CMT instead; nested exception is java.lang.IllegalStateException: Not allowed to create transaction on shared EntityManager - use Spring transactions or EJB CMT instead 
    at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:293) 
    at org.springframework.orm.jpa.aspectj.JpaExceptionTranslatorAspect.ajc$afterThrowing$org_springframework_orm_jpa_aspectj_JpaExceptionTranslatorAspect$1$18a1ac9(JpaExceptionTranslatorAspect.aj:33) 

C'è un modo per commettere la transazione e continuare? Non sono stato in grado di trovare alcun metodo che mi permetta di chiamare uno commit().

+0

È possibile ricercare se esiste un modo per far partecipare i thread spawnati alla transazione in modo che vedano i risultati senza commit. –

+0

Se il metodo è '@ Transactional', il ritorno dal metodo impegna la transazione. Quindi, perché non tornare indietro dal metodo? – Raedwald

+0

Concettualmente i test unitari non dovrebbero probabilmente essere transazionali, e con il modello di Spring non ha alcun senso pratico. Dovresti esaminare i test di integrazione usando Spring TestContext che ha gli strumenti per aiutare nelle transazioni: http://docs.spring.io/spring/docs/3.2.x/spring-framework-reference/html/testing.html # testing-tx –

risposta

37

Avevo un caso d'uso simile durante il test degli ascoltatori di eventi di ibernazione che sono chiamati solo su commit.

La soluzione era di avvolgere il codice in modo persistente in un altro metodo annotato con REQUIRES_NEW. (In un'altra classe) In questo modo viene generata una nuova transazione e un flush/commit viene emesso una volta che il metodo ritorna.

Tx prop REQUIRES_NEW

Tenete a mente che questo potrebbe influenzare tutte le altre prove! Quindi scriverli di conseguenza o è necessario assicurarsi di poter ripulire dopo il test eseguito.

+0

Brillante. Non ci avevo pensato. Devo rompere un po 'il mio metodo di prova, che non mi entusiasma troppo, ma funziona benissimo. –

+0

@MartinFrey Perché "In un'altra classe"? – Basemasta

+10

Dato che la primavera funziona con i proxy per ottenere questa funzione (anche molti altri) devi usare un'altra classe in modo che la molla sia in grado di attivare le transazioni. Una volta che sei all'interno di una classe, non passerai più attraverso il proxy. –

16

Perché non utilizzare la molla TransactionTemplate per controllare le transazioni a livello di codice? Potresti anche ristrutturare il tuo codice in modo che ogni "blocco di transazione" abbia il proprio metodo @Transactional, ma dato che si tratta di un test, opterei per il controllo programmatico delle tue transazioni.

Si noti inoltre che l'annotazione @Transactional sul runnable non funzionerà (a meno che non si utilizzi aspectj) poiché i runnables non sono gestiti da primavera!

@RunWith(SpringJUnit4ClassRunner.class) 
//other spring-test annotations; as your database context is dirty due to the committed transaction you might want to consider using @DirtiesContext 
public class TransactionTemplateTest { 

@Autowired 
PlatformTransactionManager platformTransactionManager; 

TransactionTemplate transactionTemplate; 

@Before 
public void setUp() throws Exception { 
    transactionTemplate = new TransactionTemplate(platformTransactionManager); 
} 

@Test //note that there is no @Transactional configured for the method 
public void test() throws InterruptedException { 

    final Contract c1 = transactionTemplate.execute(new TransactionCallback<Contract>() { 
     @Override 
     public Contract doInTransaction(TransactionStatus status) { 
      Contract c = contractDOD.getNewTransientContract(15); 
      contractRepository.save(c); 
      return c; 
     } 
    }); 

    ExecutorService executorService = Executors.newFixedThreadPool(5); 

    for (int i = 0; i < 5; ++i) { 
     executorService.execute(new Runnable() { 
      @Override //note that there is no @Transactional configured for the method 
      public void run() { 
       transactionTemplate.execute(new TransactionCallback<Object>() { 
        @Override 
        public Object doInTransaction(TransactionStatus status) { 
         // do whatever you want to do with c1 
         return null; 
        } 
       }); 
      } 
     }); 
    } 

    executorService.shutdown(); 
    executorService.awaitTermination(10, TimeUnit.SECONDS); 

    transactionTemplate.execute(new TransactionCallback<Object>() { 
     @Override 
     public Object doInTransaction(TransactionStatus status) { 
      // validate test results in transaction 
      return null; 
     } 
    }); 
} 

}

+0

Grazie per l'idea. Lo aveva considerato, ma sembrava un sacco di lavoro/eccessivo per un semplice problema. Avevo presunto che ci doveva essere qualcosa di molto più semplice. Inserendo il tutto in un metodo separato con Propagation.REQUIRES_NEW hai ottenuto questo risultato (vedi la risposta di @ MartinFrey). –

+1

per il mio caso funziona solo con 'transactionTemplate.setPropagationBehavior (TransactionDefinition.PROPAGATION_REQUIRES_NEW);' See codice [qui] (https://stackoverflow.com/a/46415060/548473) – GKislin

4

So che a causa di questa brutta anonima utilizzo classe interna di TransactionTemplate non sembra bello, ma quando per qualche motivo vogliamo avere un metodo di prova transazionale IMHO è la l'opzione più flessibile.

In alcuni casi (dipende dal tipo di applicazione) il modo migliore di utilizzare le transazioni nei test di primavera è uno @Transactional disattivato sui metodi di prova. Perché? Perché @Transactional potrebbe portare a molti test falsi positivi. Puoi guardare questo sample article per scoprire i dettagli. In questi casi, TransactionTemplate può essere perfetto per controllare i limiti delle transazioni quando vogliamo tale controllo.

+0

Ciao, mi chiedo se questo vale: Quando si crea un test di integrazione su un'istruzione che salva un oggetto, si consiglia di svuotare il gestore entità in modo da evitare eventuali falsi negativi, vale a dire evitare un test eseguito correttamente ma la cui operazione non riuscirebbe quando viene eseguita nella produzione. In effetti, il test può funzionare bene semplicemente perché la cache di primo livello non viene scaricata e nessuna scrittura raggiunge il database. Per evitare questo test di integrazione falsi negativi, utilizzare un flush esplicito nel corpo del test. – Stephane

+1

@StephaneEybert IMO semplicemente lo svuotamento di 'EntityManager' è più simile a un hack :-) Ma dipende dalle tue esigenze e dal tipo di test che stai facendo. Per i veri test di integrazione (che dovrebbero comportarsi esattamente come nella produzione) la risposta è: sicuramente non usare '@ Transactional' nei test. Ma esiste anche un inconveniente: devi impostare un database in uno stato ben noto prima di ogni test di questo tipo. –

Problemi correlati