TDD ile sepet kampanya motoru
Pair programming görevlerinde TDD beklentisi sık görülür
Pair programming oturumunda mülakatçı: "Bunu TDD ile yapalım. Önce testi sen yaz, ben implementasyonu, sonra değişelim." (Burada ikisini de sen yazacaksın.) Sepet satırları: record CartLine(String sku, BigDecimal unitPrice, int quantity, boolean threeForTwo).
Kurallar (sırayla uygulanır):
- 3 al 2 öde:
threeForTwoişaretli satırlarda her 3 adetten 1'i bedava. - Sepet indirimi: 1. kuraldan sonraki ara toplam 1000 TL ve üzeriyse ara toplam üzerinden %10 indirim.
- Tavan: Toplam indirim (1 + 2) 300 TL'yi geçemez.
Görevler
- Önce testleri yaz (en az 5 senaryo). Her testin neden önce yazıldığını yorumla kısaca belirt.
- Testleri geçecek en basit implementasyonu yaz: CartPricer.price(List<CartLine>) → PriceSummary(subtotal, discount, total).
- Refactor adımında kuralları genişletilebilir hale getir.
record CartLine(String sku, BigDecimal unitPrice, int quantity, boolean threeForTwo) {}
record PriceSummary(BigDecimal subtotal, BigDecimal discount, BigDecimal total) {}
// ===== CartPricerTest.java: ÖNCE BURAYI YAZ =====
class CartPricerTest {
}
// ===== CartPricer.java =====
public class CartPricer {
public PriceSummary price(List<CartLine> lines) {
throw new UnsupportedOperationException("TODO");
}
}
İpucu 1
En basit testle başla: boş sepet → 0/0/0. Sonra kampanyasız tek ürün. Her seferinde tek bir yeni davranış ekle (red → green → refactor).
İpucu 2
3 al 2 öde: bedava adet = quantity / 3 (tam sayı bölmesi). Sınırlar: 2 adet (bedava yok), 3 adet (1 bedava), 7 adet (2 bedava).
İpucu 3
%10 eşiği 1. kuraldan SONRAKİ ara toplama bakar: 3 al 2 öde ile eşiğin altına düşen sepet. Tavan testi: 5000 TL'lik sepet → %10 = 500 → 300. Refactor: List<DiscountRule> zinciri.
Referans çözümü göster
// ===== CartPricerTest.java =====
class CartPricerTest {
private final CartPricer pricer = new CartPricer();
@Test
void emptyCartCostsNothing() { // 1. en basit davranış
assertThat(pricer.price(List.of()))
.isEqualTo(new PriceSummary(money("0"), money("0"), money("0")));
}
@Test
void cartWithoutCampaignsIsSumOfLines() { // 2. temel toplam
var summary = pricer.price(List.of(line("A", "100", 2, false), line("B", "50", 1, false)));
assertThat(summary.total()).isEqualByComparingTo("250");
assertThat(summary.discount()).isEqualByComparingTo("0");
}
@ParameterizedTest(name = "{0} adet → {1} TL indirim")
@CsvSource({"2, 0", "3, 100", "5, 100", "6, 200", "7, 200"})
void threeForTwoGivesEveryThirdItemFree(int quantity, String expectedDiscount) { // 3. sınır değerler
var summary = pricer.price(List.of(line("A", "100", quantity, true)));
assertThat(summary.discount()).isEqualByComparingTo(expectedDiscount);
}
@Test
void tenPercentAppliesAtThresholdAfterThreeForTwo() { // 4. kural sırası
// 4 x 300 = 1200, 3 al 2 öde: -300 → ara toplam 900 < 1000 → %10 YOK
var below = pricer.price(List.of(line("A", "300", 4, true)));
assertThat(below.total()).isEqualByComparingTo("900");
// Tam 1000 → %10 VAR
var atThreshold = pricer.price(List.of(line("B", "1000", 1, false)));
assertThat(atThreshold.discount()).isEqualByComparingTo("100");
}
@Test
void totalDiscountIsCappedAt300() { // 5. tavan
var summary = pricer.price(List.of(line("A", "5000", 1, false))); // %10 = 500 → 300
assertThat(summary.discount()).isEqualByComparingTo("300");
assertThat(summary.total()).isEqualByComparingTo("4700");
}
private static CartLine line(String sku, String price, int qty, boolean threeForTwo) {
return new CartLine(sku, new BigDecimal(price), qty, threeForTwo);
}
private static BigDecimal money(String v) { return new BigDecimal(v).setScale(2); }
}
// ===== CartPricer.java (refactor sonrası) =====
public class CartPricer {
private static final BigDecimal MAX_DISCOUNT = new BigDecimal("300");
private final List<DiscountRule> rules = List.of(new ThreeForTwoRule(), new CartThresholdRule());
public PriceSummary price(List<CartLine> lines) {
BigDecimal subtotal = lines.stream()
.map(l -> l.unitPrice().multiply(BigDecimal.valueOf(l.quantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal discount = BigDecimal.ZERO;
for (DiscountRule rule : rules) { // kurallar sırayla, ara toplam üzerinden
discount = discount.add(rule.discount(lines, subtotal.subtract(discount)));
}
discount = discount.min(MAX_DISCOUNT).setScale(2, RoundingMode.HALF_UP);
subtotal = subtotal.setScale(2, RoundingMode.HALF_UP);
return new PriceSummary(subtotal, discount, subtotal.subtract(discount));
}
}
interface DiscountRule {
BigDecimal discount(List<CartLine> lines, BigDecimal runningTotal);
}
final class ThreeForTwoRule implements DiscountRule {
@Override
public BigDecimal discount(List<CartLine> lines, BigDecimal runningTotal) {
return lines.stream()
.filter(CartLine::threeForTwo)
.map(l -> l.unitPrice().multiply(BigDecimal.valueOf(l.quantity() / 3)))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
final class CartThresholdRule implements DiscountRule {
private static final BigDecimal THRESHOLD = new BigDecimal("1000");
private static final BigDecimal RATE = new BigDecimal("0.10");
@Override
public BigDecimal discount(List<CartLine> lines, BigDecimal runningTotal) {
return runningTotal.compareTo(THRESHOLD) >= 0 ? runningTotal.multiply(RATE) : BigDecimal.ZERO;
}
}Açıklama: neden böyle?
TDD döngüsü: Red (başarısız test) → Green (geçiren en basit kod) → Refactor (testler yeşilken tasarımı iyileştir). Mülakatta önemli olan, döngüyü görünür kılmak: "Şu an bu test kırmızı, çünkü..." / "En basit hali bu, refactor'ı testler yeşil olunca yapacağım."
- Test sırası tasarımı sürer: Boş sepet → toplam → tek kural → kural sırası → tavan. Her test tek bir yeni davranış ekler.
- Kural sırası testi (3 al 2 öde ile eşiğin altına düşmek) asıl ayırt edici test. Gereksinimdeki "sırayla uygulanır" ifadesini testle sabitlemek, sonradan birinin sırayı bozmasını engeller.
- Refactor: Yeni kampanya tipi (ör. "kategori bazlı %5") = yeni bir
DiscountRule. Open/Closed. - Belirsizlik: "%10 kargo dahil mi? Tavan satır bazında mı sepet bazında mı?" sorularını sor. Mülakatçılar netleştirme sorusu soran adayı sever.
- Testler önce ve küçük adımlarla (boş sepet, kampanyasız, tek kural...)
- 3 al 2 öde sınır değerleri (2, 3, 7 adet gibi)
- %10 eşiği: ara toplamın 1000 sınırı ve 1. kuraldan sonra hesaplanması
- 300 TL tavan testi ve implementasyonu
- BigDecimal ve yuvarlama (setScale / RoundingMode)
- Refactor: kuralları genişletilebilir yapma (DiscountRule listesi / strategy)
- Kampanyalar DB'den dinamik gelecek ve sırası değişebilecek. Tasarımın nasıl değişir?
- İki kampanya birbirini dışlıyorsa (sadece en avantajlısı uygulanır) nasıl modellersin?
- Satır bazında indirim dağıtımı (iade için hangi ürüne ne kadar indirim düştü) neden önemli?