对一个所谓 “真正的测试spring并发的事务正确性” 的证伪

rain2005 写道
楼主的代码是没有问题的,其实我想表达的意思就是楼主的测试并发大时必然死锁,从楼主的标题看是想测试spring事务的并发,楼主完全可以这样
线程1操作帐户A,B,线程2操作帐户C,D,线程3操作帐户E,F,这样才是真正的测试 spring并发的事务正确性

如果想测试程序的健壮性,如死锁可以再写测试用例。

总之保证,每个测试用例目标明确。


好了!花了点时间,完善了我对上述 rain2005 所臆想场景的模拟,并证明了此提议的荒谬。

我先给原本的测试类做了些必要的修改,然后为其添加子类 AccountTransferMultiThreadTestAccountsNotConflict。

目的:让每一个转帐线程所选取的转出(from)与转入(to)户头 与其它线程相冲突。也就是说,模拟了场景: “楼主完全可以这样 线程1操作帐户A,B,线程2操作帐户C,D,线程3操作帐户E,F,这样才是真正的测试spring并发的事务正确性。”,并证明其提议之荒谬:在这种“井水不犯河水”的操作下,根本测不了什么“spring并发的事务正确性”。

结果:在打开 spring 声明式事务处理的情况下,父测试类 AccountTransferMultiThreadTest(各转帐线程选取的户头出现冲突)与本测试类 AccountTransferMultiThreadTestAccountsNotConflict(各转帐线程选取的户头没有任何冲突) 均顺利通过,junit 显示绿色条;

在关闭 spring 声明式事务处理的情况下,父测试类 AccountTransferMultiThreadTest(各转帐线程选取的户头出现冲突) 失败,junit 显示红色条。从错误信息发现,转帐前后,所有账户总额不一致;

而在同样关闭 spring 声明式事务处理的情况下,子测试类 AccountTransferMultiThreadTestAccountsNotConflict(各转帐线程选取的户头没有任何冲突) 仍然顺利通过,junit 显示绿色条。从打印信息发现,转帐前后,所有账户总额保持一致,并且每个账户的最终余额和记录(balanceTracking)中完全相同;

证明:AccountTransferMultiThreadTestAccountsNotConflict 所模拟的这种(户头没有冲突的)操作 完全测试不了“spring并发的事务正确性”,rain2005 的提案没有任何意义;而我在顶楼(当时有小错误,后来已修正)所提出的做法才能真正达到这个目的。

希望大家在发贴的时候要谨慎些。不管你自己有多菜,总有比你更菜的人。你那些不负责任的论断,极有可能给他们造成误导。



附代码:



子测试类:AccountTransferMultiThreadTestAccountsNotConflict.java:
//import 省略

/**
 *
 * 测试类 AccountTransferMultiThreadTestAccountsNotConflict 继承了
 * 测试类 AccountTransferMultiThreadTest。
 *
 * 目的:让每一个转帐线程所选取的转出(from)与转入(to)户头不与其它线程相冲突。也就是说,
 *      模拟了 iteye.com 中某某人所臆想的场景:<b>“楼主完全可以这样 线程1操作帐户A,B,
 *      线程2操作帐户C,D,线程3操作帐户E,F,这样才是真正的测试spring并发的事务正确性。”</b>,
 *      并证明他的提议之荒谬:在这种“井水不犯河水”的操作下,根本测不了什么“spring并发的事务正确性”。
 */
public class AccountTransferMultiThreadTestAccountsNotConflict extends
		AccountTransferMultiThreadTest {

	private LinkedList<Long> accountIdsNotChosen;

	public AccountTransferMultiThreadTestAccountsNotConflict() {
		super();

		// 重新设置父类中定义的 测试户头的总数 和 测试线程总数。
		numOfAccounts = 200; // 测试户头的总数。这里,它必须是偶数。
		numOfTransfers = 100; // 测试线程总数(即转帐总次数。这里,它必须等于 测试户头总数 的一半。)
	}

	protected void setUp() throws Exception {
		super.setUp();

		// 利用“accountIdsNotChosen”,避免重复选取户头。
		accountIdsNotChosen  = new LinkedList<Long>();
		for (Long id : accountIds) {
			accountIdsNotChosen.add(id);
		}
	}

	protected void tearDown() throws Exception {
		super.tearDown();
	}

	/* (non-Javadoc)
	 * @see xiao.test.spring.testCases.AccountTransferMultiThreadTest#generateTransferThread()
	 */
	@Override
	protected TransferThread generateTransferThread() {
		return new TransferThreadAccountsNotConflict(accountService, accountIds,
				accountIdsNotChosen, balanceTracking);
	}

	/* (non-Javadoc)
	 * @see xiao.test.spring.testCases.AccountTransferMultiThreadTest#testMultiThreadTransfer()
	 */
	@Override
	public void testMultiThreadTransfer() throws Throwable {
		super.testMultiThreadTransfer();
	}

	private static class TransferThreadAccountsNotConflict extends TransferThread {
		private LinkedList<Long> accountIdsNotChosen;

		public TransferThreadAccountsNotConflict(AccountService accountService,
				long[] accountIds, LinkedList<Long> accountIdsNotChosen, Map<Long, BigDecimal> balanceTracking) {

			super(accountService, accountIds, balanceTracking);

			this.accountIdsNotChosen = accountIdsNotChosen;
			if (accountIdsNotChosen.size() <= 1) {
				throw new AppException("There are at most 1 account in 'not chosen list', cannot"
						+ " choose 2 accounts to make a transfer!");
			}
		}

		/* (non-Javadoc)
		 * @see xiao.test.spring.testCases.AccountTransferMultiThreadTest.TransferThread#generateTransferOptions()
		 */
		@Override
		protected void generateTransferOptions() {
			Random randomGenerator = new Random();

			synchronized (accountIdsNotChosen) {
				// 随机选取转出户头
				int i = randomGenerator.nextInt(accountIdsNotChosen.size());
				fromId = accountIdsNotChosen.remove(i);

				// 随机选取转入户头
				i = randomGenerator.nextInt(accountIdsNotChosen.size());
				toId = accountIdsNotChosen.remove(i);
			}

			// 随机选取转帐数额(0 ~ 149元之间)
			amount = BigDecimal.valueOf(randomGenerator.nextInt(150));
		}

		/* (non-Javadoc)
		 * @see xiao.test.spring.testCases.AccountTransferMultiThreadTest.TransferThread#runTest()
		 */
		@Override
		public void runTest() throws Throwable {
			super.runTest();
		}

	}
}



原测试类 AccountTransferMultiThreadTest.java,已做必要修改:
//import 省略

/**
 * 测试类 AccountTransferMultiThreadTest,使用了 groboutils 以实现多线程测试。
 *
 * 每个测试线程从一定数量的测试户头中随机选取一对 转出/转入 户头,然后进行一次随机数额的转帐。
 *
 * 测试户头总数由常量 numOfAccounts 设定。
 *
 * 测试线程总数由常量 numOfTransfers 设定。
 *
 */
public class AccountTransferMultiThreadTest extends TestCase {
	// 每个测试户头的初始余额为1000元
	private static final BigDecimal INIT_BALANCE = BigDecimal.valueOf(100000L, 2);
	private static int successTransfers = 0;

	protected int numOfAccounts; // 测试户头的总数
	protected int numOfTransfers; // 测试线程总数(即转帐总次数)
	private ApplicationContext context;
	protected AccountService accountService;
	protected long[] accountIds;
	protected Map<Long, BigDecimal> balanceTracking = new HashMap<Long, BigDecimal>();;

	public AccountTransferMultiThreadTest() {
		super();
		numOfAccounts = 10; // 测试户头的总数
		numOfTransfers = 300; // 测试线程总数(即转帐总次数)

		context = new ClassPathXmlApplicationContext("xiao/test/spring/*Context.xml");
		accountService = (AccountService) context.getBean("accountService");
	}

	/* (non-Javadoc)
	 * @see junit.framework.TestCase#setUp()
	 *
	 * 在setUp方法中,生成测试所需的Spring Application Context, 并在数据库中创建
	 * 一定数量的户头(Account),供多线程测试使用。
	 *
	 */
	protected void setUp() throws Exception {
		super.setUp();

		Account[] accounts = new Account[numOfAccounts];
		accountIds = new long[accounts.length];
		for (int i = 0; i < accounts.length; i++) {
			accounts[i] = new Account();
			accounts[i].setBalance(INIT_BALANCE);

			// 将当前生成的户头写入数据库
			accountService.create(accounts[i]);

			// 重要步骤!将当前生成的户头主键记录下来,以供测试线程使用
			accountIds[i] = (Long)accounts[i].getId();
		}
	}

	/* (non-Javadoc)
	 * @see junit.framework.TestCase#tearDown()
	 */
	protected void tearDown() throws Exception {
		super.tearDown();
	}

	protected Account[] getAccounts() {
		Account[] accounts = new Account[accountIds.length];
		for (int i = 0; i < accountIds.length; i++) {
			// 从数据库获取这个户头对象
			accounts[i] = accountService.findById(accountIds[i]);
		}
		// 返回户头数组
		return accounts;
	}

	protected TransferThread generateTransferThread() {
		return new TransferThread(accountService, accountIds, balanceTracking);
	}

	public void testMultiThreadTransfer() throws Throwable {

		// 验证在仅有一级缓存的情况下,用同样的主键交给 accountService.findById,它每次
		// 返回的是相等,但并不同一的实例。
		// 当然了,Account 对象的 equals 必须被正确的覆盖先。
		for (int i = 0; i < accountIds.length; i++) {
			assertEquals(accountService.findById(accountIds[i]),
					accountService.findById(accountIds[i]));
			assertNotSame(accountService.findById(accountIds[i]),
					accountService.findById(accountIds[i]));
		}

		// 获取户头对象数组
		Account[] accounts = getAccounts();
		//System.out.printf("Starting %s transfers...\n", numOfTransfers);

		// 记录测试前的所有户头总余额
		BigDecimal total1 = accountService.getTotalBalance(accounts);

		// 记录测试前的所有户头的余额
		for (Account account : accounts) {
			balanceTracking.put(account.getId(), account.getBalance());
		}

		// 生成所有测试线程
		TestRunnable[] tr = new TestRunnable[numOfTransfers];
		long start = System.currentTimeMillis();
		for (int i = 0; i < tr.length; i++) {
			tr[i] = generateTransferThread();
		}

		// 生成测试线程运行器
		MultiThreadedTestRunner mttr = new MultiThreadedTestRunner(tr);

		// 运行测试线程
		mttr.runTestRunnables();
		long used = System.currentTimeMillis() - start;
		System.out.printf("Total: %s transfers used %s milli-seconds.\n", numOfTransfers, used);

		// 获取测试后所有户头总余额
		Account[] accounts2 = getAccounts();
		BigDecimal total2 = accountService.getTotalBalance(accounts2);

		// 确认测试前后,所有户头总余额还是一致的。
		assertEquals(total1, total2);

		// 确认测试前后,所有户头余额与转帐记录相一致。
		System.out.printf("Successful transfers: %s\n", successTransfers);
		System.out.println(balanceTracking);
		for (Account account : accounts2) {
			assertEquals(balanceTracking.get(account.getId()), account.getBalance());
		}
	}

	/*
	 * 测试线程类定义
	 */
	protected static class TransferThread extends TestRunnable {
		private AccountService accountService;
		private Map<Long, BigDecimal> balanceTracking;
		protected long[] accountIds;
		protected long fromId;
		protected long toId;
		protected BigDecimal amount;

		public TransferThread(AccountService accountService,
				long[] accountIds, Map<Long, BigDecimal> balanceTracking) {
			super();
			this.accountService = accountService;
			this.accountIds = accountIds;
			this.balanceTracking = balanceTracking;
		}

		protected void generateTransferOptions() {
			Random randomGenerator = new Random();

			// 随机选取转出户头
			fromId = accountIds[
			                      randomGenerator.nextInt(accountIds.length)
			                      ];

			// 随机选取转入户头
			toId = accountIds[
			                     randomGenerator.nextInt(accountIds.length)
			                     ];

			// 确保转出、转入户头不是同一个
			while (toId == fromId) {
				toId = accountIds[
				                randomGenerator.nextInt(accountIds.length)
				                ];
			}

			// 随机选取转帐数额(0 ~ 149元之间)
			amount = BigDecimal.valueOf(randomGenerator.nextInt(150));
		}

		@Override
		public void runTest() throws Throwable {
			generateTransferOptions();

			boolean success;
			// 转帐!
			try {
				accountService.transfer(
						accountService.findById(toId),
						accountService.findById(fromId),
						amount);
				success = true;
			} catch (AppException ae) {
				// 捕捉运行时间异常“AppException”。在真实的系统中,这里必须通知用户:转帐失败,请稍后再试。
				//System.out.println("AppException:" + ae.getMessage());
				success = false;
			} catch (Throwable t) {
				// 捕捉所有异常。在真实的系统中,这里必须通知用户:转帐失败,请稍后再试。
				success = false;
			}
			if (success) {
				// 以下记录每一次成功的转帐后,被影响户头的余额。假如在 accountService.transfer 中有异常抛出,
				// 这一记录动作将不会执行。
				synchronized (balanceTracking) {
					successTransfers ++;
					BigDecimal oriFromBal = balanceTracking.get(fromId);
					BigDecimal oriToBal = balanceTracking.get(toId);
					System.out.printf("Successful transfer no.%s: account[%s] (bal: %s) -> account[%s] (bal: %s),"
							+ " amount (%s)\n", successTransfers, fromId, oriFromBal, toId, oriToBal, amount);
					balanceTracking.put(fromId, oriFromBal.subtract(amount));
					balanceTracking.put(toId, oriToBal.add(amount));
				}
			}
		}
	}
}

你可能感兴趣的:(spring,多线程,F#,JUnit)