Landroid

[Flutter] TDD 사용하기 본문

플러터

[Flutter] TDD 사용하기

silso 2021. 5. 21. 19:43

정말 고맙게도 공식문서에서 테스트하는 방법이 아주 잘 작성되어 있다.

 

플러터에는 크게 3가지의 테스팅 기법이 있습니다. 유닛 테스트, 위젯 테스트, 통합 테스트.

하지만 이것을 가지고 TDD를 작성하기는 매우 어렵습니다.

(플러터 TDD 정보가 너무 부족해....)

 

그래서 해외 자료들과 공식문서, 국내 블로그(특히 티스토리)를 참고하여 플러터에서 TDD를 적용하는 방법에 대해 설명하겠습니다.

 

1. 유닛 테스트 (UnitTest)

유닛 테스트, 단위 테스트라고 불리는 이 테스팅 기법은 메소드, 클래스 같은 작은 단위를 테스트할 때 쓰입니다.

 

하지만 서버통신이나 DB 접근이 필요한 테스트일 경우, 의존성 때문에 작성하기 더 어려워집니다.

그래서 Mockito 같은 테스트 프레임워크로 의존성을 줄이고 테스트를 작성할 수 있습니다.

 

유닛 테스트를 구현하기 위해서는 기본적인 테스트 패키지인 'test' 패키지를 추가해서 구현하여야 합니다.

추가로 의존성을 필요한다면 'mockito' 패키지를 추가하여야 합니다.

 

(공식문서가 여기보다 더 잘 정리되어 있다는 건 비밀)

 

단위 테스트 소개

새로운 기능을 추가하거나 기존 기능을 변경했을 때, 앱이 여전히 제대로 동작한다는 것을어떻게 보장할 수 있을까요? 테스트 코드를 작성하세요.단위 테스트는 하나의 함수, 메서드 혹은 클래

flutter-ko.dev

 

 

 

Mockito를 사용하여 의존성들에 대해 mock 객체 생성하기

어떠한 경우에는 단위 테스트가 웹 서비스나 데이터베이스로부터 데이터를 가져오는 역할을수행하는 특정 클래스에 의존하는 경우가 있습니다. 이럴 때는 다음과 같은 이유로 인해 테스트가

flutter-ko.dev

 

심화

 

Flutter - 유닛 테스트 해보기

flutter_testing 플러터 유닛 테스팅 유닛 테스팅 (Unit Testing) 첨엔 핫리로딩의 효율성이 정말 좋다보니 테스팅을 할 필요성을 못 느꼈었는데요. 점차 코드를 추가하다보니 테스트를 해야

software-creator.tistory.com

 

 

2. 위젯 테스트 (WidgetTest)

위젯 테스트는 간단히 말해서 UI 테스트라고 부를 수 있습니다.

플러터에 위젯 테스트는 위젯 UI가 예상대로 보여지는지 상호작용이 정상적으로 동작하는지 검증하는 테스트입니다.

 

위젯 테스트를 구현하기 위해서는 기본적인 위젯 테스트 패키지인 'flutter_test' 패키지를 추가해서 구현하여야 합니다.

UI 테스트라고 해서 구현하기 어렵다고 생각하는 사람이 많은데 (사실 어려운 거 맞아) 

간단하게 다음과 같은 메커니즘을 가지고 있다.

 

1. WidgetTester에 pumpWidget으로 테스트하고 싶은 위젯을 추가한다.

2-1. find나 key를 활용해서 추가한 위젯에서 테스트하고 싶은 자식 위젯을 찾는다.

2-2. WidgetTester으로 탭, 드래그, 키보드 입력과 같은 행동으로 위젯의 동작을 정의한다.

3. expect함수로 해당 자식 위젯이 원하는 결과를 보이고 있는지 검사한다.

 

 

An introduction to widget testing

 

flutter-ko.dev

 

 

Find widgets

 

flutter-ko.dev

 

Tap, drag, and enter text

 

flutter-ko.dev

 

 

3. 통합 테스트 (IntergrationTest)

출처: http://blog.naver.com/PostView.nhn?blogId=jtum&logNo=220930341302

 

 

위에서 설명하듯이 통합 테스트는 개별 클래스, 함수, 위젯 등 각 개별 요소들이 실제 기기에서 어떻게 같이 어우러져 동작하는지 검증하는 테스팅 기법입니다.

 

통합 테스트를 구현하기 위해서는 기본적인 통합 테스트 패키지인 'flutter_driver' 패키지를 추가해서 구현하여야 합니다.

 

통합 테스트 소개

단위 테스트와 위젯 테스트는 개별 클래스, 함수, 위젯을 테스트하기 유용합니다. 하지만,이러한 각 개별 요소들이 실제 기기에서 어떻게 같이 어우러져 동작하는지 테스트하거나실제 기기에서

flutter-ko.dev

 

 

 

 

 

 

TDD란?

1. TDD는 간단하게 비지니스 코드를 작성하기 전에 테스트 코드를 먼저 만드는 것 개발 방법론입니다.

2. TDD는 최대한 빨리 실패를 하고 보완해가며 조금 더 코드가 완벽해지도록 개발되는 방식입니다.

3. 플러터에서는 비지니스 코드는 물론 UI 코드 조차 테스트를 먼저 만들고 구현해 나갑니다.

 

TDD 작성 예시

 

How To Use Test-Driven Development (TDD) with Flutter | ArcTouch

Instructions how to use test-driven development (TDD) with Flutter from ArcTouch's cross-platform app engineering team.

arctouch.com

 

위 블로그는 플러터 TDD에 대해 가장 잘 정리되어있습니다.

그래서 해당 블로그를 번역하여 플러터에서 TDD를 어떻게 적용하여 개발하는지 예제와 함께 각 단계를 설명하도록 하겠습니다. 

(각 단계는 절대적인 것이 아니며 상황에 따라 순서가 변경될 수 있습니다.)

 

 

Step 0:  노트앱

간단하게 노트앱을 플러터와 TDD로 구현하겠습니다.

이제 아래 4가지 기능들을 가진 앱을 만들 것입니다.

  • 노트를 리스트로 보여주기
  • 노트 생성
  • 노트 수정
  • 노트 삭제

 

* 테스트 코드 보기 어려우시면 제목이라도 보셔서 무엇을 하는지 정도 확인하시길 바랍니다.

 

Step 1: 플러터에서 사용할 데이터 모델에 대한 TDD 테스트 작성

앱의 가장 핵심적인 데이터 모델에 대한 테스트 코드를 만들겠습니다.

현재 만들 앱은 노트앱이기 때문에 노트에 대한 데이터 클래스를 만들기 위해 CRUD가 가능해야 하고 제목과 본문이 필수적으로 있어야 합니다.

이제 위에 두 조건을 참고하여 테스트 코드부터 작성하겠습니다.

(추가로 해당 데이터 모델 클래스 이름은 NotesCubit입니다.)

 

import 'package:test/test.dart';

void main() {
  group('Notes Cubit', () {
    test('default is empty', () {
      var cubit = NotesCubit();
      expect(cubit.state.notes, []);
    });


    test('add notes', () {
      var title = 'title';
      var body = 'body';
      var cubit = NotesCubit();
      
      cubit.createNote(title, body);
      expect(cubit.state.notes.length, 1);
      expect(cubit.state.notes.first, Note(1, title, body));
    });


    test('delete notes', () {
      var cubit = NotesCubit();
      cubit.createNote('title', 'body');
      cubit.createNote('another title', 'another body');
      cubit.deleteNote(1);
      
      expect(cubit.state.notes.length, 1);
      expect(cubit.state.notes.first.id, 2);
    });


    test('update notes', () {
      var cubit = NotesCubit();
      cubit.createNote('title', 'body');
      cubit.createNote('another title', 'another body');
      cubit.createNote('yet another title', 'yet another body');

      var newTitle = 'my cool note';
      var newBody = 'my cool note body';
      cubit.updateNote(2, newTitle, newBody);
      
      expect(cubit.state.notes.length, 3);
      expect(cubit.state.notes[1], Note(2, newTitle, newBody));
    });
  });
}

 

Step 2:  데이터 모델 구현

테스트 코드를 만들면 빨간 줄이 뜰 것입니다.

(보통은 테스트 코드에서 빨간 줄 뜨면 즉시 클래스 만들어서 구현하는 게 일반적이지만 -_-)

그래서 빨간 줄을 제거하기 위해 데이터 모델을 구현해야 합니다.

테스트에서 필요한 정보들을 구현함으로써 최소한의 구현으로 코드가 깔끔해지는 혜택을 받을 수 있습니다.

(나만 안 느껴지는 거니? ^^)

 

// lib/mode/note.dart
// 핵심 데이터 모델
class Note extends Equatable {
  final int id;
  final String title;
  final String body;

  Note(this.id, this.title, this.body);

  @override
  List<Object> get props => [id, title, body];
}

// lib/cubit/notes_cubit.dart
// 데이터 모델의 상태
class NotesState {
  final UnmodifiableListView notes;
  NotesState(this.notes);
}

// 데이터 모델을 관리하는 컨트롤러
class NotesCubit extends Cubit<NotesState> {
  List _notes = [];
  int autoIncrementId = 0;

  NotesCubit() : super(NotesState(UnmodifiableListView([])));

  void createNote(String title, String body) {
    _notes.add(Note(++autoIncrementId, title, body));
    emit(NotesState(UnmodifiableListView(_notes)));
  }

  void deleteNote(int id) {
    _notes = _notes.where((element) => element.id != id).toList();
    emit(NotesState(UnmodifiableListView(_notes)));
  }

  void updateNote(int id, String title, String body) {
    var noteIndex = _notes.indexWhere((element) => element.id == id);
    _notes.replaceRange(noteIndex, noteIndex + 1, [Note(id, title, body)]);
    emit(NotesState(UnmodifiableListView(_notes)));
  }
}

 

Step 3: UI 테스트 코드 작성

비지니스 코드뿐만 아니라 UI 코드도 예외 없이 테스트 코드부터 작성해야 합니다.

 

하지만 이런 걱정을 할 수 있습니다.

???: 으악 화면마다 UI가 겁나게 복잡한데 어떻게 테스트부터 작성할 수 있는 거야!!!!

 

우리는 UI가 복잡해지면서 위젯이 계속 계단을 만들어가는 모습을 눈살이 찌푸려 가는데도 가만히 놔둘 리가 없죠 ^^

그래서 UI를 핵심 기능이나 위젯 단위로 분리해서 구현합니다. (설마 아직도 한 파일 안에 코드를 다 작성하는 사람은 없겠지?)

그래야 테스트하기도 쉽고 UI 구현하는데도 문제가 없습니다.

일단 우리가 구현할 UI는 굉장히 단순하기 때문에 화면 단위로 테스트 코드를 작성하겠습니다.

 

// test/widget/home_page_test.dart

import 'package:flutter_test/flutter_test.dart';

void main() {
  group('Home Page', () {
    _pumpTestWidget(WidgetTester tester, NotesCubit cubit) => tester.pumpWidget(
      MaterialApp(
        home: MyHomePage(
          title: 'Home',
          notesCubit: cubit,
        ),
      ),
    );

    testWidgets('empty state', (WidgetTester tester) async {
      await _pumpTestWidget(tester, NotesCubit());
      expect(find.byType(ListView), findsOneWidget);
      expect(find.byType(ListTile), findsNothing);
    });

    testWidgets('updates list when a note is added',
        (WidgetTester tester) async {
      var notesCubit = NotesCubit();
      await _pumpTestWidget(tester, notesCubit);
      var expectedTitle = 'note title';
      var expectedBody = 'note body';
      notesCubit.createNote(expectedTitle, expectedBody);
      notesCubit.createNote('another note', 'another note body');
      await tester.pump();

      expect(find.byType(ListView), findsOneWidget);
      expect(find.byType(ListTile), findsNWidgets(2));
      expect(find.text(expectedTitle), findsOneWidget);
      expect(find.text(expectedBody), findsOneWidget);
    });

    testWidgets('updates list when a note is deleted', (WidgetTester tester) async {
      var notesCubit = NotesCubit();
      await _pumpTestWidget(tester, notesCubit);
      var expectedTitle = 'note title';
      var expectedBody = 'note body';
      notesCubit.createNote(expectedTitle, expectedBody);
      notesCubit.createNote('another note', 'another note body');
      await tester.pump();

      notesCubit.deleteNote(1);
      await tester.pumpAndSettle();
      expect(find.byType(ListView), findsOneWidget);
      expect(find.byType(ListTile), findsOneWidget);
      expect(find.text(expectedTitle), findsNothing);
    });
  });
}

 

Step 4: HomePage UI 구현

ㅈㄱㄴ (말 그대로 노트를 리스트 뷰로 보여주는 UI를 구현합니다.)

 

// lib/home_page.dart

class MyHomePage extends StatelessWidget {
  final NotesCubit notesCubit;
  final String title;

  MyHomePage({Key key, this.title, this.notesCubit}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: BlocBuilder<NotesCubit, NotesState>(
        cubit: notesCubit,
        builder: (context, state) => ListView.builder(
          itemCount: state.notes.length,
          itemBuilder: (context, index) {
            var note = state.notes[index];
            return ListTile(
              title: Text(note.title),
              subtitle: Text(note.body),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        tooltip: 'Add',
        child: Icon(Icons.add),
      ),
    );
  }
}

 

Step 5: 새로운 페이지 생성

갑자기 뜬금없이 왜 새 페이지를 만드는 이유는앞 단계에서 FAB을 눌렀을 때 노트를 만드는 것을 예상할 수 있습니다.

 

이렇게 화면 간의 이동이 있을 때 테스트 코드를 작성하고 새로운 UI를 만듭니다.그래서 아래와 같이 FAB을 눌렀을 때 테스트 코드를 추가하고 HomePage에 화면을 이동하도록 Navigator를 사용합니다.

 

// test/widget/home_page_test.dart

// ...
testWidgets('navigate to note page', (WidgetTester tester) async {
  var notesCubit = NotesCubit();
  await _pumpTestWidget(tester, notesCubit);
  await tester.tap(find.byType(FloatingActionButton));
  await tester.pumpAndSettle();
  expect(find.byType(NotePage), findsOneWidget);
});
// ...

 

// lib/note_page.dart

class NotePage extends StatelessWidget {
  final NotesCubit notesCubit;

  MyHomePage({Key key, this.notesCubit}) : super(key: key);

  @override
  Widget build(BuildContext context) => Container();
}

// lib/home_page.dart
// Here we add a new method to our widget
// ...
_goToNotePage(BuildContext context) => Navigator.push(
        context,
        MaterialPageRoute(
          builder: (context) => NotePage(
            notesCubit: notesCubit,
          ),
        ),
      );
// ...

// And update our floatingActionButton
floatingActionButton: FloatingActionButton(
        onPressed: () => _goToNotePage(context),
        tooltip: 'Add',
        child: Icon(Icons.add),
      ),

 

Step 6: 노트 생성을 위한 테스트 코드 작성

노트를 작성하기 위해서는 많은 것이 필요하지 않습니다.

노트를 생성하는 NotePage에서는 단순히 텍스트 필드 두 개를 두어도 충분합니다.

이제 두 텍스트 필드에 대한 테스트 코드를 작성하겠습니다.

 

// test/widget/note_page_test.dart

void main() {
  group('Note Page', () {
    _pumpTestWidget(WidgetTester tester, NotesCubit cubit) =>
      tester.pumpWidget(
        MaterialApp(
          home: NotePage(
            notesCubit: cubit
          ),
        ),
      );

    testWidgets('empty state', (WidgetTester tester) async {
      await _pumpTestWidget(tester, NotesCubit());
      expect(find.text('Enter your text here...'), findsOneWidget);
      expect(find.text('Title'), findsOneWidget);
    });

    testWidgets('create note', (WidgetTester tester) async {
      var cubit = NotesCubit();
      await _pumpTestWidget(tester, cubit);
      await tester.enterText(find.byKey(ValueKey('title')), 'hi');
      await tester.enterText(find.byKey(ValueKey('body')), 'there');
      await tester.tap(find.byType(RaisedButton));
      await tester.pumpAndSettle();
      expect(cubit.state.notes, isNotEmpty);
      var note = cubit.state.notes.first;
      expect(note.title, 'hi');
      expect(note.body, 'there');
      expect(find.byType(NotePage), findsNothing);
    });
  });
}

 

Step 7: NotePage UI 구현

테스트를 구현했으니 UI를 구현해야죠.

 

// lib/note_page.dart

class NotePage extends StatefulWidget {
  final NotesCubit notesCubit;

  const NotePage({Key key, this.notesCubit}) : super(key: key);

  @override
  _NotePageState createState() => _NotePageState();
}

class _NotePageState extends State {
  final _titleController = TextEditingController();
  final _bodyController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(
              key: ValueKey('title'),
              controller: _titleController,
              autofocus: true,
              decoration: InputDecoration(hintText: 'Title'),
            ),
            Expanded(
              child: TextField(
                key: ValueKey('body'),
                controller: _bodyController,
                keyboardType: TextInputType.multiline,
                maxLines: 500,
                decoration:
                    InputDecoration(hintText: 'Enter your text here...'),
              ),
            ),
            RaisedButton(
              child: Text('Ok'),
              onPressed: () => _finishEditing(),
            )
          ],
        ),
      ),
    );
  }

  _finishEditing() {
    widget.notesCubit.createNote(_titleController.text, _bodyController.text);
Navigator.pop(context);
  }

  @override
  void dispose() {
    super.dispose();
    _titleController.dispose();
    _bodyController.dispose();
  }
}

 

Step 8: HomePage와 NotePage에 대한 테스트 코드 작성

제목이 당황스럽지만 그냥 노트 수정 기능을 구현하는 단계입니다.

(왜 이렇게 제목을 지었지?)

 

// lib/test/widget/home_page_test.dart
// ...
// In our home page tests we can assert the list tapping action
testWidgets('navigate to note page in edit mode',
    (WidgetTester tester) async {
  var notesCubit = NotesCubit();
  await _pumpTestWidget(tester, notesCubit);
  var expectedTitle = 'note title';
  var expectedBody = 'note body';
  notesCubit.createNote(expectedTitle, expectedBody);
  await tester.pump();
  await tester.tap(find.byType(ListTile));
  await tester.pumpAndSettle();
  expect(find.byType(NotePage), findsOneWidget);
  expect(find.text(expectedTitle), findsOneWidget);
  expect(find.text(expectedBody), findsOneWidget);
});
// ...

// lib/test/widget/home_page_test.dart

void main() {
  group('Note Page', () {
    _pumpTestWidget(WidgetTester tester, NotesCubit cubit, {Note note}) =>
      tester.pumpWidget(
        MaterialApp(
          home: NotePage(
            notesCubit: cubit,
            note: note,
          ),
        ),
      );

    // ...

    testWidgets('create in edit mode', (WidgetTester tester) async {
      var note = Note(1, 'my note', 'note body');
      await _pumpTestWidget(tester, NotesCubit(), note: note);
      expect(find.text(note.title), findsOneWidget);
      expect(find.text(note.body), findsOneWidget);
    });

    testWidgets('edit note', (WidgetTester tester) async {
      var cubit = NotesCubit()..createNote('my note', 'note body');
      await _pumpTestWidget(tester, cubit, note: cubit.state.notes.first);
      await tester.enterText(find.byKey(ValueKey('title')), 'hi');
      await tester.enterText(find.byKey(ValueKey('body')), 'there');
      await tester.tap(find.byType(RaisedButton));
      await tester.pumpAndSettle();
      expect(cubit.state.notes, isNotEmpty);
      var note = cubit.state.notes.first;
      expect(note.title, 'hi');
      expect(note.body, 'there');
      expect(find.byType(NotePage), findsNothing);
    });
  });
}
// lib/home_page.dart

class MyHomePage extends StatelessWidget {
  // ...
  // Adding the ListTile tap action:
            return ListTile(
              title: Text(note.title),
              subtitle: Text(note.body),
              onTap: () => _goToNotePage(context, note: note),
            );
  // ...
  // Sending the Note to the next page:
  _goToNotePage(BuildContext context, {Note note}) => Navigator.push(
        context,
        MaterialPageRoute(
          builder: (context) => NotePage(
            notesCubit: notesCubit,
            note: note,
          ),
        ),
      );
}

// lib/note_page.dart

class NotePage extends StatefulWidget {
  final NotesCubit notesCubit;
  final Note note;

  const NotePage({Key key, this.notesCubit, this.note}) : super(key: key);

  @override
  _NotePageState createState() => _NotePageState();
}

class _NotePageState extends State {
  final _titleController = TextEditingController();
  final _bodyController = TextEditingController();

  @override
  void initState() {
    super.initState();
    if (widget.note == null) return;
    _titleController.text = widget.note.title;
    _bodyController.text = widget.note.body;
  }

  // ...

  _finishEditing() {
    if (widget.note != null) {
      widget.notesCubit.updateNote(
        widget.note.id, _titleController.text, _bodyController.text);
    } else {
      widget.notesCubit.createNote(_titleController.text, _bodyController.text);
    }
    Navigator.pop(context);
  }

  // ...
}

 

Step 9: 삭제 기능 구현

여기까지 오셨으면 대강 기능을 구현할 때마다 플러터에서 TDD로 어떻게 구현하는지 감이 오실 거라고 생각합니다.

 

// test/widget/note_page_test.dart

void main() {
  group('Note Page', () {
    _pumpTestWidget(WidgetTester tester, NotesCubit cubit, {Note note}) =>
      tester.pumpWidget(
        MaterialApp(
          home: NotePage(
            notesCubit: cubit,
            note: note,
          ),
        ),
      );

    testWidgets('empty state', (WidgetTester tester) async {
      await _pumpTestWidget(tester, NotesCubit());
      expect(find.text('Enter your text here...'), findsOneWidget);
      expect(find.text('Title'), findsOneWidget);
      var widgetFinder = find.widgetWithIcon(IconButton, Icons.delete);
      var deleteButton = widgetFinder.evaluate().single.widget as IconButton;
      expect(deleteButton.onPressed, isNull);
    });

    testWidgets('create note', (WidgetTester tester) async {
      var cubit = NotesCubit();
      await _pumpTestWidget(tester, cubit);
      await tester.enterText(find.byKey(ValueKey('title')), 'hi');
      await tester.enterText(find.byKey(ValueKey('body')), 'there');
      await tester.tap(find.byType(RaisedButton));
      await tester.pumpAndSettle();
      expect(cubit.state.notes, isNotEmpty);
      var note = cubit.state.notes.first;
      expect(note.title, 'hi');
      expect(note.body, 'there');
      expect(find.byType(NotePage), findsNothing);
    });

    testWidgets('create in edit mode', (WidgetTester tester) async {
      var note = Note(1, 'my note', 'note body');
      await _pumpTestWidget(tester, NotesCubit(), note: note);
      expect(find.text(note.title), findsOneWidget);
      expect(find.text(note.body), findsOneWidget);
      var widgetFinder = find.widgetWithIcon(IconButton, Icons.delete);
      var deleteButton = widgetFinder.evaluate().single.widget as IconButton;
      expect(deleteButton.onPressed, isNotNull);
    });

    testWidgets('edit note', (WidgetTester tester) async {
      var cubit = NotesCubit()..createNote('my note', 'note body');
      await _pumpTestWidget(tester, cubit, note: cubit.state.notes.first);
      await tester.enterText(find.byKey(ValueKey('title')), 'hi');
      await tester.enterText(find.byKey(ValueKey('body')), 'there');
      await tester.tap(find.byType(RaisedButton));
      await tester.pumpAndSettle();
      expect(cubit.state.notes, isNotEmpty);
      var note = cubit.state.notes.first;
      expect(note.title, 'hi');
      expect(note.body, 'there');
      expect(find.byType(NotePage), findsNothing);
    });

    testWidgets('delete note', (WidgetTester tester) async {
      var cubit = NotesCubit()..createNote('my note', 'note body');
      await _pumpTestWidget(tester, cubit, note: cubit.state.notes.first);
      await tester.tap(find.byType(IconButton));
      await tester.pumpAndSettle();
      expect(cubit.state.notes, isEmpty);
      expect(find.byType(NotePage), findsNothing);
    });
  });
}
// lib/note_page.dart

  // ...
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        actions: [
          IconButton(
            icon: Icon(Icons.delete),
            onPressed: widget.note != null ? _deleteNote : null,
          )
        ],
      ),
      // ...
  }

  // ...
  _deleteNote() {
    widget.notesCubit.deleteNote(widget.note.id);
    Navigator.pop(context);
  }
  // ...
}

 

 

Reference

 

Flutter Clean Architecture & TDD Course - Reso Coder

This website uses cookies so that we can provide you with the best user experience possible. Cookie information is stored in your browser and performs functions such as recognising you when you return to our website and helping our team to understand which

resocoder.com

 

 

Flutter Test-Driven Development

Find out how to build a Flutter App with a Test-Driven Development approach. Get used to TDD with the Red-Green-Refactor rhythm, and never…

medium.com

 

 

TDD 학습 및 실습 정리1(TDD의 기본사용법)

테스트 주도 개발 TDD 실천법과 도구 책을 참고했습니다. 바로 얍! 하고 하는 방법보다 테스트 주도 개발의 의의와 순서를 음미하고 싶어서 정리했습니다. 참고 TDD란 1. TDD를 주도한 켄트 벡트벡

otrodevym.tistory.com

 

Comments