用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/Objective-Arts/lens-dist --skill test-doubles命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
基于 SOC 职业分类
正在显示 SKILL.md
| name | test-doubles |
| description | xUnit test patterns, test doubles, and test smells |
Apply Gerard Meszaros' patterns for writing maintainable, reliable tests.
Test Double (generic term)
├── Dummy - Passed but never used
├── Stub - Provides canned answers
├── Spy - Records calls for verification
├── Mock - Verifies expected interactions
└── Fake - Working implementation (simplified)
Purpose: Fill required parameters that won't be used.
// The logger is required but not relevant to this test
@Test
void calculateTotal_ignoresLogger() {
Logger dummyLogger = null; // Or a no-op logger
Calculator calc = new Calculator(dummyLogger);
assertEquals(10, calc.add(4, 6));
}
Purpose: Provide predetermined responses.
// Stub returns canned data - no verification
public class StubPriceService implements PriceService {
@Override
public double getPrice(String productId) {
return 99.99; // Always returns this
}
}
@Test
void order_calculatesWithPrice() {
PriceService stubPrices = new StubPriceService();
Order order = new Order(stubPrices);
order.addItem("WIDGET", 2);
assertEquals(199.98, order.getTotal(), 0.01);
}
Purpose: Record interactions for later verification.
public class SpyEmailService implements EmailService {
private final List<String> sentTo = new ArrayList<>();
@Override
public void send(String to, String message) {
sentTo.add(to); // Record the call
}
// Inspection method
public boolean wasSentTo(String email) {
return sentTo.contains(email);
}
public int getSendCount() {
return sentTo.size();
}
}
@Test
void orderConfirmation_sendsEmail() {
SpyEmailService spy = new SpyEmailService();
OrderService orders = new OrderService(spy);
orders.complete(testOrder);
assertTrue(spy.wasSentTo("customer@test.com"));
assertEquals(1, spy.getSendCount());
}
Purpose: Verify expected behavior (pre-programmed expectations).
// Using Mockito
@Test
void checkout_chargesCard() {
PaymentGateway mockGateway = mock(PaymentGateway.class);
when(mockGateway.charge(any(), any())).thenReturn(true);
Checkout checkout = new Checkout(mockGateway);
checkout.process(order, card);
// Verify interaction happened
verify(mockGateway).charge(card, Money.of(99.99));
}
Purpose: Working but simplified implementation.
// Fake in-memory database
public class FakeUserRepository implements UserRepository {
private final Map<Long, User> users = new HashMap<>();
private long nextId = 1;
@Override
public User save(User user) {
if (user.getId() == null) {
user.setId(nextId++);
}
users.put(user.getId(), user);
return user;
}
@Override
public Optional<User> findById(Long id) {
return Optional.ofNullable(users.get(id));
}
@Override
public List<User> findAll() {
return new ArrayList<>(users.values());
}
}
@Test
void userService_savesAndRetrieves() {
UserRepository fakeRepo = new FakeUserRepository();
UserService service = new UserService(fakeRepo);
User saved service.createUser();
service.findById(saved.getId());
assertEquals(, found.getEmail());
}
| Double | Use When | Verify |
|---|---|---|
| Dummy | Parameter needed but irrelevant | Nothing |
| Stub | Need controlled indirect inputs | State only |
| Spy | Need to verify calls happened | After exercise |
| Mock | Behavior verification is primary | During exercise |
| Fake | Need realistic behavior | State only |
@Test
void withdraw_reducesBalance() {
// 1. SETUP (Arrange)
Account account = new Account();
account.deposit(100);
// 2. EXERCISE (Act)
account.withdraw(30);
// 3. VERIFY (Assert)
assertEquals(70, account.getBalance());
// 4. TEARDOWN (Cleanup) - often implicit
}
Each test creates its own test data.
@Test
void test1() {
User user = createTestUser(); // Fresh for this test
// ...
}
@Test
void test2() {
User user = createTestUser(); // Fresh for this test too
// ...
}
@BeforeEach
void setUp() {
this.testUser = createTestUser(); // Shared setup
}
@Test
void test1() {
// Uses testUser
}
@Test
void test2() {
// Uses same testUser structure, but reset state
}
Only set up what the test actually needs.
// WRONG: Over-specified fixture
@Test
void getName_returnsName() {
User user = new User();
user.setId(1L);
user.setEmail("test@example.com");
user.setName("Alice");
user.setAge(30);
user.setCreatedAt(new Date());
user.setRole(Role.ADMIN);
assertEquals("Alice", user.getName()); // Only name matters!
}
// RIGHT: Minimal fixture
@Test
void getName_returnsName() {
User user = new User();
user.setName("Alice");
assertEquals("Alice", user.getName());
}
Symptom: Test breaks when unrelated code changes.
// FRAGILE: Tests internal structure
@Test
void order_hasItems() {
order.addItem(item);
assertEquals(1, order.items.size()); // Breaks if items becomes List
}
// ROBUST: Tests behavior
@Test
void order_hasItems() {
order.addItem(item);
assertTrue(order.containsItem(item));
}
Symptom: Can't understand what test does without reading deeply.
// OBSCURE
@Test
void test1() {
X x = new X(1, 2, 3, "a", true, null);
assertEquals(6, x.calc());
}
// CLEAR
@Test
void calc_sumsPriceQuantityAndTax() {
Product product = aProduct()
.withPrice(1)
.withQuantity(2)
.withTax(3)
.build();
assertEquals(6, product.calculateTotal());
}
Symptom: One test verifies too many things.
// EAGER: Testing everything at once
@Test
void userWorkflow() {
User user = userService.create("alice@test.com");
assertNotNull(user.getId());
assertEquals("alice@test.com", user.getEmail());
user.setName("Alice");
userService.update(user);
assertEquals("Alice", userService.findById(user.getId()).getName());
userService.delete(user.getId());
assertNull(userService.findById(user.getId()));
}
// FOCUSED: One behavior per test
@Test void create_assignsId() { /* ... */ }
@Test void create_setsEmail() { /* ... */ }
@Test void update_changesName() { /* ... */ }
@Test void delete_removesUser() { /* ... */ }
Symptom: Test depends on external data not visible in test.
// MYSTERY: Where does "user-123" come from?
@Test
void findUser_returnsUser() {
User user = userService.findById("user-123");
assertEquals("Alice", user.getName());
}
// EXPLICIT: Test data is visible
@Test
void findUser_returnsUser() {
User alice = userService.create("Alice");
User found = userService.findById(alice.getId());
assertEquals("Alice", found.getName());
}
Symptom: Production code has if (testing) checks.
// WRONG: Test mode in production
public class PaymentService {
public void charge(Card card, Money amount) {
if (System.getProperty("test.mode") != null) {
return; // Skip in tests
}
gateway.charge(card, amount);
}
}
// RIGHT: Inject the dependency
public class PaymentService {
private final PaymentGateway gateway;
public PaymentService(PaymentGateway gateway) {
this.gateway = gateway;
}
public void charge(Card card, Money amount) {
gateway.charge(card, amount);
}
}
// In tests: new PaymentService(fakeGateway)
// Verify the resulting state
@Test
void deposit_increasesBalance() {
account.deposit(50);
assertEquals(150, account.getBalance()); // Check state
}
// Verify interactions occurred
@Test
void transfer_callsBothAccounts() {
Account from = mock(Account.class);
Account to = mock(Account.class);
transferService.transfer(from, to, 50);
verify(from).withdraw(50); // Check behavior
verify(to).deposit(50);
}
// Verify the change, not absolute value
@Test
void deposit_increasesBalanceByAmount() {
int before = account.getBalance();
account.deposit(50);
assertEquals(before + 50, account.getBalance()); // Delta
}
// Encapsulate complex assertions
public static void assertValidOrder(Order order) {
assertNotNull(order.getId(), "Order must have ID");
assertFalse(order.getItems().isEmpty(), "Order must have items");
assertTrue(order.getTotal() > 0, "Order total must be positive");
}
@Test
void createOrder_returnsValidOrder() {
Order order = orderService.create(items);
assertValidOrder(order);
}
| Smell | Solution |
|---|---|
| Fragile test | Test behavior, not structure |
| Obscure test | Better names, test builders |
| Eager test | Split into focused tests |
| Mystery guest | Inline test data |
| Slow test | Use fakes instead of real deps |
| Erratic test | Remove shared state |