Flutter

Widget Test Flutter

1. Load library

dev_dependencies:
  flutter_test:
    sdk: flutter
  integration_test:
    sdk: flutter

2. Implement Page

2.1 Session Helper

Tạo class SessionHelper xử lý lưu trữ thông tin đăng nhập

class SessionHelper {
  static Member? login(String email, String password) {
    if (email == "thanh@ceb.net.vn" && password == "123") {
      Random rand = Random();
      return Member(
        id: rand.nextInt(99999) + 9999,
        name: "thanh",
        email: email,
        role: "member",
      );
    }
    return null;
  }
  static Future<void> save(Member obj) async {
    SharedPreferences shared = await SharedPreferences.getInstance();
    shared.setString('member', jsonEncode(obj.toMap()));
  }
  static Future<Member?> get member async {
    SharedPreferences shared = await SharedPreferences.getInstance();
    String? result = shared.getString('member');
    return result != null ? Member.fromMap(jsonDecode(result)) : null;
  }
  static Future<void> clear() async {
    SharedPreferences shared = await SharedPreferences.getInstance();
    await shared.remove('member');
  }
}

2.2 Home Page

Tạo class HomePage là trang chủ hiện thị thông tin trước và sau khi đăng nhập

class HomePage extends StatefulWidget {
  final String title;
  const HomePage({super.key, required this.title});
  @override
  State<StatefulWidget> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
  Member? _member;
  @override
  void initState() {
    super.initState();
    _initData();
  }
  Future<void> _initData() async {
    Member? obj = await SessionHelper.member;
    setState(() {
      _member = obj;
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: Column(
        children: [
          if (_member != null) ...[
            ElevatedButton.icon(onPressed: _logout, label: Text('Logout')),
            ElevatedButton.icon( onPressed: _profile, label: Text(_member!.name)),
          ] else ...[
            ElevatedButton.icon(onPressed: _login, label: Text('Login')),
          ],
        ],
      ),
    );
  }
  void _profile() {}
  void _logout() async {
    await SessionHelper.clear();
    if (!mounted) return;
    ScaffoldMessenger.of(context).hideCurrentSnackBar();
    setState(() {
      _member = null;
    });
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Logout Success')));
  }
  void _login() async {
    var result = await Navigator.pushNamed(context, '/login');
    if (result == true) {
      await _initData();
    }
  }
}

2.3 Login Page

Tạo class Login Page xử lý đăng nhập thông tin người dùng.

class LoginPage extends StatefulWidget {
  final String _title;
  const LoginPage({super.key, required this._title});
  @override
  State<StatefulWidget> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
  String? _msg;
  final TextEditingController _emailController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();
  final _formKey = GlobalKey<FormState>();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget._title)),
      body: Form(
        key: _formKey,
        child: Column(
          children: [
            TextFormField(
              validator: _validateEmail,
              key: Key("email"),
              controller: _emailController,
              decoration: InputDecoration(labelText: 'Email'),
            ),
            TextFormField(
              validator: _validatePassword,
              key: Key("password"),
              controller: _passwordController,
              decoration: InputDecoration(labelText: 'Password'),
            ),
            ElevatedButton(
              key: Key("login"),
              onPressed: _login,
              child: Text('Login'),
            ),
            if (_msg != null) Text(_msg!),
          ],
        ),
      ),
    );
  }
  String? _validatePassword(String? password) {
    if (password == null || password.isEmpty) return "Please enter password";
    if (password.length < 3) return "Please enter password min 3 character";
    return null;
  }
  String? _validateEmail(String? email) {
    if (email == null || email.isEmpty) return "Please enter email";
    RegExp regExp = RegExp(r'^[a-z][a-z0-9]+@[a-z0-9]{2,}(\.[a-z]{2,})+$');
    return regExp.hasMatch(email) ? null : "Email invalid";
  }
  void _login() async {
    if (_formKey.currentState!.validate()) {
      String email = _emailController.text.trim();
      String password = _passwordController.text.trim();
      Member? member = SessionHelper.login(email, password);
      if (member != null) {
        await SessionHelper.save(member);
        if (!mounted) return;
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: const Text('Login Success')));
        Navigator.pop(context, true);
      } else {
        _msg = "Login Failed";
      }
    }
  }
}

2.4 main

Trong hàm main xử lý quản lý định tuyến giữa các trang HomePage và LoginPage

void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(colorScheme: .fromSeed(seedColor: Colors.deepPurple)),
      initialRoute: '/',
      routes: {
        '/': (context) => HomePage(title: 'Home Page'),
        '/login': (context) => LoginPage(title: 'Login Page'),
      },
    );
  }
}

3. Integration Test

Tạo thư mục integration_test và tạo file app_test.dart với nội dung như sau:

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
  setUp(() {
    SharedPreferences.setMockInitialValues({});
  });
  testWidgets('Login Test', (tester) async {
    app.main();
    await tester.pumpAndSettle();
    expect(find.text('Login'), findsOneWidget);
    expect(find.text('Logout'), findsNothing);
    await tester.tap(find.text('Login'));
    await tester.pumpAndSettle();
    expect(find.text('Login Page'), findsOne);
    await tester.enterText(find.byKey(const Key('email')), 'thanh@ceb.net.vn');
    await tester.enterText(find.byKey(const Key('password')), '123');
    await tester.tap(find.byKey(const Key('login')));
    //await tester.pump();
    await tester.pumpAndSettle();
    //Login Success
    expect(find.text('Login Success'), findsOneWidget);
    expect(find.text('Logout'), findsOneWidget);
    expect(find.text('Login'), findsNothing);
    expect(find.text('thanh'), findsOne);
    await tester.tap(find.text('Logout'));
    await tester.pumpAndSettle();
    await tester.pump();
    expect(find.text('Login'), findsOne);
    expect(find.text('Logout'), findsNothing);
    expect(find.byType(SnackBar), findsOneWidget);
    expect(find.text('Logout Success'), findsOneWidget);
  });
}