三层 Spring 应用、测试与 AOP 详解
1. 三层 Spring 应用代码示例
在开发三层 Spring 应用时,我们会涉及到控制器、服务层和数据访问层等不同层次的代码编写。以下是一个控制器类的部分代码示例:
public void initBinder(WebDataBinder binder) throws Exception {
binder.registerCustomEditor(String.class,
new StringTrimmerEditor(false));
}
@RequestMapping(method = RequestMethod.GET)
public String setupForm(
@RequestParam(required = false, value = "id") Integer id,
ModelMap model) {
model.addAttribute(Constants.FRUIT, id == null ? new FruitMap()
: fruitRepository.getFruitType(id));
return "role/form";
}
@Autowired @Qualifier("fruitRepository")
private FruitTypeRepository fruitRepository;
@Autowired @Qualifier("fruitValidator")
FruitValidator validator;
这段代码主要实现了以下功能:
-
initBinder
方法:注册了一个自定义的字符串编辑器,用于去除字符串前后的空格。
-
setupForm
方法:处理 GET 请求,根据传入的
id
参数从
fruitRepository
中获取水果类型,并将其添加到模型中,最后返回视图名称。
-
fruitRepository
和
fruitValidator
:通过
@Autowired
注解进行依赖注入。
2. 测试框架与测试分类
2.1 测试框架选择
我们使用 JUnit 4.5 作为测试框架,并结合注解来编写测试用例。同时,为了在测试前后准备和清理数据库,我们使用了 DbUnit。JUnit 的官方网站是 http://www.junit.org,DbUnit 的官方网站是 http://dbunit.sourceforge.net。
2.2 测试分类
主要有以下三种测试类型:
-
单元测试
:针对单个类或方法进行测试,确保其功能的正确性。
-
集成测试
:测试对象在运行时的条件下,如数据库连接、与其他对象的交互等。
-
综合测试套件
:将所有测试用例组合在一起运行。
2.3 测试主类
AllTests
package it.freshfruits;
import it.freshfruits.conf.dbunit.DbUnit;
import it.freshfruits.domain.entity.CustomerUnitTest;
import it.freshfruits.domain.entity.FruitTypeUnitTest;
import it.freshfruits.domain.entity.OrderUnitTest;
import it.freshfruits.domain.factory.CustomerFactoryTest;
import it.freshfruits.domain.repository.CustomerRepositoryTest;
import it.freshfruits.domain.repository.OrderRepositoryTest;
import it.freshfruits.domain.service.SupplyServiceTest;
import it.freshfruits.domain.vo.AddressUnitTest;
import it.freshfruits.domain.vo.ContactInformationUnitTest;
import it.freshfruits.domain.vo.OrderItemUnitTest;
import it.freshfruits.ui.CustomerControllerTest;
import it.freshfruits.ui.FruitControllerTest;
import it.freshfruits.utils.ValidationUtilsTest;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
@RunWith(Suite.class)
@SuiteClasses( { AddressUnitTest.class, ContactInformationUnitTest.class,
CustomerUnitTest.class, FruitTypeUnitTest.class,
ValidationUtilsTest.class, OrderUnitTest.class,
OrderItemUnitTest.class, CustomerRepositoryTest.class,
OrderRepositoryTest.class, CustomerFactoryTest.class,
CustomerControllerTest.class, FruitControllerTest.class,
SupplyServiceTest.class })
public class AllTests {
@BeforeClass
public static void setUp() throws Exception {
SpringFactory.setUpXmlWebApplicationContext();
}
@AfterClass
public static void tearDown() throws Exception {
DbUnit.closeConnection();
SpringFactory.destroyXmlWebApplicationContext();
}
}
AllTests
类作为测试套件的主类,使用
@RunWith(Suite.class)
注解标记,通过
@SuiteClasses
注解指定要运行的测试类。
@BeforeClass
方法在所有测试用例执行前初始化 Spring 应用上下文,
@AfterClass
方法在所有测试用例执行后关闭数据库连接并销毁应用上下文。
3. 领域类的单元测试
3.1
AddressUnitTest
package it.freshfruits.domain.vo;
import static org.junit.Assert.assertEquals;
import it.freshfruits.domain.vo.Address;
import it.freshfruits.domain.vo.AddressImpl;
import org.junit.Test;
public class AddressUnitTest {
@Test
public void testConstructorCorrect() {
Address address = new AddressImpl.Builder("Viale Europa",
"Cagliari",
"Italy").build();
assertEquals(address.getCity(), "Cagliari");
assertEquals(address.getState(), "Italy");
assertEquals(address.getStreet(), "Viale Europa");
}
@Test(expected = IllegalArgumentException.class)
public void testNullStreet() {
new AddressImpl.Builder(null, "Cagliari", "Italy").build();
}
@Test(expected = IllegalArgumentException.class)
public void testNullCity() {
new AddressImpl.Builder("Viale Europa", null, "Italy").build();
}
@Test(expected = IllegalArgumentException.class)
public void testNullState() {
new AddressImpl.Builder("Viale Europa", "Cagliari", null).
build();
}
}
AddressUnitTest
类用于测试
Address
值对象的构造函数的正确性。通过
@Test
注解标记测试方法,使用
assertEquals
方法进行断言。对于可能抛出异常的情况,使用
@Test(expected = IllegalArgumentException.class)
注解进行验证。
3.2 其他领域类的单元测试
类似地,还有
ContactInformationUnitTest
、
OrderItemUnitTest
、
FruitTypeUnitTest
、
CustomerUnitTest
和
OrderUnitTest
等类,分别对不同的领域类进行单元测试。以下是部分测试方法的示例:
// ContactInformationUnitTest
@Test
public void testConstructorCorrect() {
ContactInformation contact = new ContactInformationImpl.Builder(
"+39070123456", "3391234567", "", "foo[at]yahoo[dot]it")
.build();
assertEquals(contact.getEmail(), "foo[at]yahoo[dot]it");
assertEquals(contact.getFaxNumber(), "");
assertEquals(contact.getMobilePhoneNumber(), "3391234567");
assertEquals(contact.getPhoneNumber(), "+39070123456");
}
// OrderItemUnitTest
@Test
public void testConstructorCorrect() {
FruitType fruit = new FruitTypeImpl.Builder("orange", new
Integer(2),
new BigDecimal("0.20")).build();
OrderItem item = new OrderItemImpl.Builder(fruit, 20, "1").
build();
assertEquals(item.getFruitType(), fruit);
assertEquals(item.getAmountItem().toString(), "4.00");
assertEquals(item.getQuantity(), new Integer(20));
}
3.3 单元测试流程
graph LR
A[开始] --> B[初始化测试对象]
B --> C[执行测试方法]
C --> D{是否抛出预期异常}
D -- 是 --> E[测试通过]
D -- 否 --> F{断言是否通过}
F -- 是 --> E
F -- 否 --> G[测试失败]
E --> H[结束测试]
G --> H
4. 控制器的单元测试
4.1
CustomerControllerTest
package it.freshfruits.ui;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import it.freshfruits.conf.dbunit.DBCustomer;
import it.freshfruits.conf.dbunit.DBOrder;
import it.freshfruits.conf.dbunit.DBOrderItems;
import it.freshfruits.domain.entity.Customer;
import it.freshfruits.domain.entity.CustomerImpl;
import it.freshfruits.domain.entity.FruitType;
import it.freshfruits.domain.entity.FruitTypeImpl;
import it.freshfruits.domain.vo.OrderItemImpl;
import it.freshfruits.util.Constants;
import java.math.BigDecimal;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.servlet.ModelAndView;
public class CustomerControllerTest {
private CustomerController customerController;
private Customer customer;
private FruitType fruit;
@Before
public void setup() {
customerController = new CustomerController();
}
@After
public void tearDown() {
customerController = null;
customer = null;
fruit = null;
}
@Test
public void create() throws Exception {
customer = new CustomerImpl.Builder("max", "26").address(
"Viale Europa", "Cagliari", "Italy").contactInfo(
"+39070123456", "3391234567", "", "foo[at]yahoo[dot]it")
.build();
MockHttpServletRequest req = new MockHttpServletRequest();
req.setMethod("GET");
req.setAttribute(Constants.CUSTOMER, customer);
ModelAndView modelAndView = customerController.create(req);
assertEquals(modelAndView.getViewName(), "customer/create");
assertTrue(modelAndView.getModel().containsKey("result"));
}
// 其他测试方法...
}
CustomerControllerTest
类用于测试
CustomerController
控制器。使用
@Before
注解在每个测试方法执行前进行初始化,使用
@After
注解在每个测试方法执行后进行清理。通过
MockHttpServletRequest
模拟 HTTP 请求,调用控制器的方法并进行断言。
4.2 控制器单元测试流程
graph LR
A[开始] --> B[初始化控制器和测试数据]
B --> C[创建模拟 HTTP 请求]
C --> D[调用控制器方法]
D --> E[获取返回结果]
E --> F{结果是否符合预期}
F -- 是 --> G[测试通过]
F -- 否 --> H[测试失败]
G --> I[清理测试数据]
H --> I
I --> J[结束测试]
5. 集成测试
5.1
CustomerRepositoryTest
package it.freshfruits.domain.repository;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import it.freshfruits.SpringFactory;
import it.freshfruits.application.repository.CustomerRepository;
import it.freshfruits.conf.dbunit.DBCustomer;
import it.freshfruits.domain.entity.Customer;
import it.freshfruits.domain.entity.CustomerImpl;
import it.freshfruits.domain.entity.CustomerView;
import it.freshfruits.domain.vo.Address;
import it.freshfruits.domain.vo.ContactInformation;
import it.freshfruits.util.Constants;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.dao.DataAccessException;
public class CustomerRepositoryTest {
@Before
public void setUp() throws Exception {
ctx = SpringFactory.getXmlWebApplicationContext();
repo = (CustomerRepository) ctx.getBean("customerRepository");
}
@After
public void tearDown() throws Exception {
db.cleanDb();
ctx = null;
repo = null;
}
@Test
public void testInsertCustomer() {
Customer customer = new CustomerImpl.Builder("max", Constants.
ID_NEW)
.address("Viale Europa", "Cagliari", "Italy")
.contactInfo("+39070123456", "3391234567", "",
"foo[at]yahoo[dot]it").build();
try {
repo.insertCustomer(customer);
} catch (DataAccessException e) {
fail("unexpected exception");
}
return;
}
// 其他测试方法...
}
CustomerRepositoryTest
类用于测试
CustomerRepository
在数据库中的操作。在
@Before
方法中初始化 Spring 应用上下文并获取
CustomerRepository
实例,在
@After
方法中清理数据库。通过
@Test
注解标记不同的测试方法,对插入、查询、更新和删除等操作进行测试。
5.2
OrderRepositoryTest
package it.freshfruits.domain.repository;
import static org.junit.Assert.*;
import it.freshfruits.SpringFactory;
import it.freshfruits.application.repository.OrderRepository;
import it.freshfruits.conf.dbunit.DBCustomer;
import it.freshfruits.conf.dbunit.DBFruitType;
import it.freshfruits.conf.dbunit.DBOrder;
import it.freshfruits.conf.dbunit.DBOrderItems;
import it.freshfruits.domain.entity.Order;
import it.freshfruits.domain.entity.OrderImpl;
import it.freshfruits.domain.vo.OrderItem;
import it.freshfruits.util.Constants;
import it.freshfruits.util.DateTimeUtils;
import java.math.BigDecimal;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
public class OrderRepositoryTest {
@Before
public void setUp() throws Exception {
ctx = SpringFactory.getXmlWebApplicationContext();
repo = (OrderRepository) ctx.getBean("orderRepository");
}
@After
public void tearDown() throws Exception {
ctx = null;
repo = null;
}
@Test
public void testInsertOrder() {
DBCustomer dbCustomer = new DBCustomer();
DBOrder dbOrders = new DBOrder();
dbCustomer.prepareDb();
Order order = new OrderImpl.Builder(Constants.ID_NEW,
DateTimeUtils
.getDateNowToNextDays(2).toDate(), "26").amount(
new BigDecimal("5000")).build();
try {
String id = repo.insertOrder(order);
Order retrieve = repo.getOrder(id, "26");
assertNotNull(retrieve);
} catch (Exception e) {
fail("exception unexpected");
}
dbOrders.cleanDb();
dbCustomer.cleanDb();
}
// 其他测试方法...
}
OrderRepositoryTest
类用于测试
OrderRepository
在数据库中的操作,与
CustomerRepositoryTest
类似,也是在测试前后进行数据库的准备和清理工作。
5.3 集成测试流程
graph LR
A[开始] --> B[初始化 Spring 应用上下文]
B --> C[准备数据库数据]
C --> D[执行测试操作]
D --> E{操作是否成功}
E -- 是 --> F[验证结果]
E -- 否 --> G[测试失败]
F --> H{结果是否符合预期}
H -- 是 --> I[测试通过]
H -- 否 --> G
I --> J[清理数据库数据]
G --> J
J --> K[销毁 Spring 应用上下文]
K --> L[结束测试]
6. 运行集成测试的注意事项
为了使领域类的注入正常工作,进行集成测试时需要使用 Spring 提供的 Load Time Weaver 的 JAR 文件。在 Spring 发行版的
dist/weaving
文件夹中有三个 JAR 文件:
-
spring-agent.jar
-
spring-aspects.jar
-
spring-tomcat-weaver.jar
运行测试时,需要使用
spring-agent.jar
并向虚拟机传递以下参数:
-javaagent:<path to jar>/spring-agent.jar
可以通过 Eclipse 的配置窗口设置该参数来运行 JUnit 测试。在 Linux 系统上,也可以通过相应的命令行参数进行设置。
综上所述,通过单元测试和集成测试可以确保三层 Spring 应用的正确性和稳定性。单元测试关注单个类或方法的功能,集成测试关注对象在运行时的交互和数据库操作。合理使用测试框架和工具,严格按照测试流程进行测试,可以提高代码的质量和可维护性。
7. 测试框架与工具总结
7.1 测试框架
| 框架名称 | 作用 | 官方网站 |
|---|---|---|
| JUnit 4.5 | 用于编写和运行单元测试,通过注解标记测试方法 | http://www.junit.org |
| DbUnit | 用于在测试前后准备和清理数据库,确保测试环境的一致性 | http://dbunit.sourceforge.net |
7.2 测试工具
在进行集成测试时,使用 Spring 提供的 Load Time Weaver 的 JAR 文件:
-
spring-agent.jar
:用于实现类的加载时织入,确保领域类的注入正常工作。
-
spring-aspects.jar
:包含 Spring AOP 的切面实现。
-
spring-tomcat-weaver.jar
:用于在 Tomcat 服务器中进行织入。
运行集成测试时,需要向虚拟机传递
-javaagent:<path to jar>/spring-agent.jar
参数,可以通过 Eclipse 配置窗口或 Linux 命令行进行设置。
8. 测试代码示例总结
8.1 单元测试代码示例
以下是部分单元测试代码的总结:
// AddressUnitTest
@Test
public void testConstructorCorrect() {
Address address = new AddressImpl.Builder("Viale Europa", "Cagliari", "Italy").build();
assertEquals(address.getCity(), "Cagliari");
assertEquals(address.getState(), "Italy");
assertEquals(address.getStreet(), "Viale Europa");
}
// ContactInformationUnitTest
@Test
public void testConstructorCorrect() {
ContactInformation contact = new ContactInformationImpl.Builder("+39070123456", "3391234567", "", "foo[at]yahoo[dot]it").build();
assertEquals(contact.getEmail(), "foo[at]yahoo[dot]it");
assertEquals(contact.getFaxNumber(), "");
assertEquals(contact.getMobilePhoneNumber(), "3391234567");
assertEquals(contact.getPhoneNumber(), "+39070123456");
}
// OrderItemUnitTest
@Test
public void testConstructorCorrect() {
FruitType fruit = new FruitTypeImpl.Builder("orange", new Integer(2), new BigDecimal("0.20")).build();
OrderItem item = new OrderItemImpl.Builder(fruit, 20, "1").build();
assertEquals(item.getFruitType(), fruit);
assertEquals(item.getAmountItem().toString(), "4.00");
assertEquals(item.getQuantity(), new Integer(20));
}
8.2 控制器单元测试代码示例
// CustomerControllerTest
@Test
public void create() throws Exception {
customer = new CustomerImpl.Builder("max", "26").address("Viale Europa", "Cagliari", "Italy").contactInfo("+39070123456", "3391234567", "", "foo[at]yahoo[dot]it").build();
MockHttpServletRequest req = new MockHttpServletRequest();
req.setMethod("GET");
req.setAttribute(Constants.CUSTOMER, customer);
ModelAndView modelAndView = customerController.create(req);
assertEquals(modelAndView.getViewName(), "customer/create");
assertTrue(modelAndView.getModel().containsKey("result"));
}
8.3 集成测试代码示例
// CustomerRepositoryTest
@Test
public void testInsertCustomer() {
Customer customer = new CustomerImpl.Builder("max", Constants.ID_NEW).address("Viale Europa", "Cagliari", "Italy").contactInfo("+39070123456", "3391234567", "", "foo[at]yahoo[dot]it").build();
try {
repo.insertCustomer(customer);
} catch (DataAccessException e) {
fail("unexpected exception");
}
return;
}
// OrderRepositoryTest
@Test
public void testInsertOrder() {
DBCustomer dbCustomer = new DBCustomer();
DBOrder dbOrders = new DBOrder();
dbCustomer.prepareDb();
Order order = new OrderImpl.Builder(Constants.ID_NEW, DateTimeUtils.getDateNowToNextDays(2).toDate(), "26").amount(new BigDecimal("5000")).build();
try {
String id = repo.insertOrder(order);
Order retrieve = repo.getOrder(id, "26");
assertNotNull(retrieve);
} catch (Exception e) {
fail("exception unexpected");
}
dbOrders.cleanDb();
dbCustomer.cleanDb();
}
9. 测试流程对比
9.1 单元测试流程
graph LR
A[开始] --> B[初始化测试对象]
B --> C[执行测试方法]
C --> D{是否抛出预期异常}
D -- 是 --> E[测试通过]
D -- 否 --> F{断言是否通过}
F -- 是 --> E
F -- 否 --> G[测试失败]
E --> H[结束测试]
G --> H
9.2 控制器单元测试流程
graph LR
A[开始] --> B[初始化控制器和测试数据]
B --> C[创建模拟 HTTP 请求]
C --> D[调用控制器方法]
D --> E[获取返回结果]
E --> F{结果是否符合预期}
F -- 是 --> G[测试通过]
F -- 否 --> H[测试失败]
G --> I[清理测试数据]
H --> I
I --> J[结束测试]
9.3 集成测试流程
graph LR
A[开始] --> B[初始化 Spring 应用上下文]
B --> C[准备数据库数据]
C --> D[执行测试操作]
D --> E{操作是否成功}
E -- 是 --> F[验证结果]
E -- 否 --> G[测试失败]
F --> H{结果是否符合预期}
H -- 是 --> I[测试通过]
H -- 否 --> G
I --> J[清理数据库数据]
G --> J
J --> K[销毁 Spring 应用上下文]
K --> L[结束测试]
从上述流程图可以看出,不同类型的测试流程有一定的差异。单元测试主要关注单个类或方法的功能,控制器单元测试需要模拟 HTTP 请求,而集成测试则需要处理 Spring 应用上下文和数据库操作。
10. 测试的重要性及最佳实践
10.1 测试的重要性
- 保证代码质量 :通过单元测试和集成测试,可以及时发现代码中的 bug,确保代码的正确性和稳定性。
- 提高可维护性 :测试代码可以作为代码的文档,帮助开发人员理解代码的功能和使用方法,便于后续的维护和扩展。
- 促进团队协作 :测试可以作为团队成员之间沟通的桥梁,确保各个模块之间的交互正常,提高团队的协作效率。
10.2 最佳实践
- 编写全面的测试用例 :覆盖各种可能的输入和边界条件,确保代码在不同情况下都能正常工作。
- 及时更新测试代码 :随着代码的修改和功能的增加,及时更新测试代码,保证测试的有效性。
- 自动化测试 :使用持续集成工具(如 Jenkins)自动化运行测试,及时发现代码中的问题。
- 遵循测试流程 :严格按照单元测试、控制器单元测试和集成测试的流程进行测试,确保测试的全面性和准确性。
11. 总结与展望
通过对三层 Spring 应用的单元测试和集成测试的介绍,我们了解了如何使用 JUnit 4.5 和 DbUnit 等工具进行测试,以及不同类型测试的流程和注意事项。合理的测试可以提高代码的质量和可维护性,确保应用在运行时的稳定性。
在未来的开发中,可以进一步探索更多的测试技术和工具,如性能测试、安全测试等,以满足不同场景下的测试需求。同时,结合敏捷开发和 DevOps 理念,将测试融入到整个开发流程中,实现快速迭代和持续交付。
总之,测试是软件开发过程中不可或缺的一部分,通过不断地学习和实践,我们可以提高测试的效率和质量,为用户提供更加稳定和可靠的应用。
超级会员免费看
168万+

被折叠的 条评论
为什么被折叠?



