使用ScalaMock测试ScyllaDB操作代码时遇NullPointerException
解决ScalaMock测试StudentRepository时的NullPointerException问题
错误原因
直接mock StudentDB 会触发其构造函数执行,而StudentDB继承的Database父类在初始化时需要依赖CassandraConnection的有效实例。ScalaMock创建mock对象时未传入合法的CassandraConnection,导致父类构造过程中访问null对象抛出NullPointerException。
另外,StudentRepository实际依赖的是StudentDB内部Student schema的操作(select.all().fetch、store(student).future()),而非StudentDB本身,直接mock整个StudentDB不仅容易触发构造错误,也不符合测试的最小依赖原则。
解决方案
推荐采用抽象依赖接口的方式,将数据访问逻辑封装到独立接口中,让StudentRepository依赖接口而非具体的StudentDB类,这样测试时可以轻松mock接口实现。
步骤1:抽象数据访问接口
定义一个包含所需操作的接口,让StudentDB实现该接口:
import scala.concurrent.Future import com.datastax.driver.core.ResultSet trait StudentDao { def getAllStudents: Future[List[StudentData]] def insertStudent(student: StudentData): Future[ResultSet] } class StudentDB(override val connector: CassandraConnection) extends Database[StudentDB](connector) with StudentDao { object Student extends StudenSchema with Connector // 实现接口方法 override def getAllStudents: Future[List[StudentData]] = { Student.select.all().fetch } override def insertStudent(student: StudentData): Future[ResultSet] = { Student.store(student).future() } }
步骤2:修改StudentRepository依赖接口
调整StudentRepository,使其依赖抽象的StudentDao而非具体的StudentDB:
class StudentRepository @Inject()(studentDao: StudentDao) extends StudentRepositoryTrait { override def getStudents: Future[List[StudentData]] = { studentDao.getAllStudents } override def insertStudentData(student: StudentData): Future[ResultSet] = { studentDao.insertStudent(student) } }
步骤3:编写Mock测试用例
现在可以直接mockStudentDao接口,指定方法的返回结果并验证调用:
import scala.concurrent.Future import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import org.scalamock.scalatest.MockFactory import com.datastax.driver.core.ResultSet class RepositoryMockTesting extends AnyFlatSpec with Matchers with MockFactory { val mockStudentDao: StudentDao = mock[StudentDao] val studentRepository = new StudentRepository(mockStudentDao) val testStudent: StudentData = StudentData(Some("Zaman Khan"), Some("1"), Some(""), Some(""), None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None) val mockStudentList = List(testStudent) val mockResultSet = mock[ResultSet] "StudentRepository" should "fetch non-empty student list" in { // 预期getAllStudents方法被调用,返回模拟的学生列表 (mockStudentDao.getAllStudents _).expects().returning(Future.successful(mockStudentList)) val result = studentRepository.getStudents result.map { list => list should not be empty list shouldEqual mockStudentList } } it should "successfully insert student data" in { // 预期insertStudent方法被传入testStudent,返回模拟的ResultSet (mockStudentDao.insertStudent _).expects(testStudent).returning(Future.successful(mockResultSet)) // 预期ResultSet的wasApplied方法返回true (mockResultSet.wasApplied _).expects().returning(true) val result = studentRepository.insertStudentData(testStudent) result.map { rs => rs.wasApplied() shouldBe true } } }
补充说明
如果不想大幅修改现有代码,也可以尝试创建一个带有mockCassandraConnection的StudentDB实例,再mock其内部Student对象的方法,但这种方式复杂度较高,不如抽象接口的方式简洁易维护。
内容的提问来源于stack exchange,提问作者Asif khAn
相关产品推荐
相关产品推荐

