Java多SQLite连接问题:JUnit测试遇SQLITE_BUSY数据库锁定错误
这个问题的核心原因其实和JUnit的测试执行机制、SQLite的锁特性,以及你没有关闭数据库连接的操作都有关系,咱们一步步拆解:
为什么会出现数据库锁?
JUnit的实例创建逻辑:JUnit默认会为每个
@Test方法创建一个全新的TestSuite类实例。也就是说,第一个测试方法执行时创建了一个DatabaseHandler实例(持有一个数据库连接),测试结束后这个连接因为没有被关闭,会一直持有数据库的资源;第二个测试方法执行时,又会创建一个新的DatabaseHandler实例(第二个数据库连接),当第二个连接尝试对数据库执行写操作(truncate、insert)时,第一个连接还没释放锁,SQLite的独占写锁机制就会触发SQLITE_BUSY错误。SQLite的锁机制:SQLite是文件型数据库,写操作需要获取独占锁。如果有一个连接还持有数据库的锁(哪怕没有显式开启事务),后续的写操作就会被阻塞,直到锁被释放。而你的
DatabaseHandler没有任何关闭连接的逻辑,连接会一直存活到JVM垃圾回收,这就导致多个连接同时争抢数据库锁。
具体的解决方案
方案1:用JUnit生命周期注解管理连接的创建与关闭
在每个测试方法执行前初始化DatabaseHandler,测试结束后强制关闭连接,确保每个测试的资源都被清理:
import org.junit.AfterEach; import org.junit.BeforeEach; import org.junit.Test; import static org.junit.Assert.assertEquals; public class TestSuite { private DatabaseHandler dbh; @BeforeEach public void setUp() { dbh = new DatabaseHandler("testdb"); } @AfterEach public void tearDown() { try { if (dbh.statement != null) { dbh.statement.close(); } if (dbh.connection != null) { dbh.connection.close(); } } catch (SQLException e) { System.err.println(e.getMessage()); } } @Test public void testInsertEntryDuplicateNoChange() { Listing listing = new Listing(); dbh.truncateEntryTable(); listing.setId("12345"); listing.setName("John"); listing.setAge(24); dbh.insertEntry(listing); assertEquals(3, dbh.insertEntry(listing)); } @Test public void testInsertEntryDuplicatePriceChange() { Listing listing = new Listing(); dbh.truncateEntryTable(); listing.setId("54321"); listing.setName("John"); listing.setAge(34); // 这里你原来的代码写了listing.Age(34),应该是setAge,注意修正! dbh.insertEntry(listing); listing.setAge(55); assertEquals(1, dbh.insertEntry(listing)); } }
另外注意:你的第二个测试里有个语法错误listing.Age(34),应该是listing.setAge(34),这可能也是测试报错的潜在原因之一。
方案2:使用静态连接确保全局唯一
如果希望整个测试类共享一个数据库连接(减少连接创建开销),可以把dbh改成静态变量,用@BeforeClass和@AfterClass来管理连接的生命周期,但要注意测试之间的数据隔离(比如每个测试前都truncate表):
import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import static org.junit.Assert.assertEquals; public class TestSuite { private static DatabaseHandler dbh; @BeforeClass public static void setUpClass() { dbh = new DatabaseHandler("testdb"); } @AfterClass public static void tearDownClass() { try { if (dbh.statement != null) { dbh.statement.close(); } if (dbh.connection != null) { dbh.connection.close(); } } catch (SQLException e) { System.err.println(e.getMessage()); } } // 测试方法和之前一致,注意每个测试前truncate表保证隔离 @Test public void testInsertEntryDuplicateNoChange() { // ... 代码省略 } @Test public void testInsertEntryDuplicatePriceChange() { // ... 代码省略 } }
额外建议:完善DatabaseHandler的资源管理
最好在DatabaseHandler里添加一个close()方法,把关闭连接和语句的逻辑封装起来,这样代码更整洁:
public class DatabaseHandler { // 你的现有成员变量:connection、statement等 public DatabaseHandler(String dbName) { // 现有构造函数逻辑 } public void close() { try { if (statement != null) { statement.close(); } if (connection != null) { connection.close(); } } catch (SQLException e) { System.err.println(e.getMessage()); } } // 其他方法:insertEntry、truncateEntryTable等 }
这样在tearDown里只需要调用dbh.close()即可。
内容的提问来源于stack exchange,提问作者Cornel Verster

